<commit_before>/*************************************************************************
 float.js 1.0 Copyright (c) 2013 Heinrich Schweitzer
 Contact me at hgs@hgsweb.de
 This copyright notice MUST stay intact for use.

 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
 'Software'), to deal in the Software without restriction, including
 without limitation the rights to use, copy, modify, merge, publish,
 distribute, sublicense, and/or sell copies of the Software, and to
 permit persons to whom the Software is furnished to do so, subject to
 the following conditions:

 The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 ***************************************************************************/

/*//////////////////////////////////////////////////////////////
 Creates divs to mimic the table header.

 This header is allways displayed at the top of the table, if
 the original header is scrolled out of view.

 To use this funtion your table must have an id speficied.
 Once alle tables are rendered you call floatHeader(tableId,head),
 this returns the header object.

 /////////////////////////////////////////////////////////////*/

function floatHeader(tableId, head) {
    'use strict';
    function setAtt(s, o) {
        var opt;
        for (opt in o) {
            s[opt] = o[opt];
        }
        return s;
    }
    function absPos(obj) {// return absolute x,y position of obj
        var ob, pos = {};
        pos.x = obj.offsetLeft;
        pos.y = obj.offsetTop;
        ob = obj.offsetParent;
        while (ob.tagName !== 'BODY') {
            pos.x += ob.offsetLeft;
            pos.y += ob.offsetTop;
            ob = ob.offsetParent;
        }
        return pos;
    }
    function createDivHead(mytable, x) {
        var div = document.createElement('div');
        setAtt(div, {id: 'float_' + mytable.id,
            className: 'outerFloatHead'}
        );
        setAtt(div.style, {zIndex: 15,
            width: mytable.clientWidth + 'px',
            left: x + 'px'
        }
        );
        return div;
    }
    function createDivCornerHead(mytable, x) {
        var div = document.createElement('div');
        setAtt(div, {id: 'floatcorner_' + mytable.id,
            className: 'floatCorner'}
        );
        setAtt(div.style, {zIndex: 15,
            left: x + 'px',
            height: mytable.rows[0].cells[0].clientHeight + 'px'}
        );
        return div;
    }
    function createDivLeftColumn(mytable, x) {
        var div = document.createElement('div');
        setAtt(div, {id: 'floatleftcolumn_' + mytable.id,
            className: 'outerFloatHead'}
        );
        setAtt(div.style, {zIndex: 12,
            left: x + 'px',
            height: mytable.rows[0].cells[0].clientHeight + 'px'}
        );
        return div;
    }
    function createHeaderCell(theCell, top, ci) {
        var div = document.createElement('div');
        updateHeaderCell(div, theCell, top, ci);
        return div;
    }
    function updateHeaderCell(div, theCell, top, ci) {
        setAtt(div, {className: 'floatHead',
            innerHTML: theCell.innerHTML,
            cellIndex: ci}
        );
        setAtt(div.style, {
            left: theCell.offsetLeft + 'px',
            top: top + 'px',
            width: theCell.clientWidth + 'px'
        }
        );
        return div;
    }
    function createLeftColumn(theCell, top, ci, ri) {
        var div = document.createElement('div');
        updateLeftColumn(div, theCell, top, ci, ri);
        return div;
    }
    function updateLeftColumn(div, theCell, top, ci, ri) {
        setAtt(div, {className: 'floatCol',
            innerHTML: theCell.innerHTML,
            cellIndex: ci,
            rowIndex: ri}
        );
        setAtt(div.style, {
            left: theCell.offsetLeft + 'px',
            top: top + 'px',
            width: theCell.clientWidth + 'px',
            height: theCell.clientHeight + 'px'}
        );
        return div;
    }
    function setLeftColumnGeometry(head) {
        setAtt(lc.style, {
            top: absPos(mytable.rows[head.ncpth.length].cells[0]).y + 'px',
            left: floatPos.x + 'px',
            height: mytable.clientHeight - mytable.rows[head.ncpth.length].offsetTop + 'px',
            width: lcw + 'px'});
    }
    function setTopLeftCornerGeometry() {
        var nr, nc;
        nr = mytable.rows.length;
        nc = mytable.rows[nr - 1].cells.length;
        setAtt(tlc, {
            rightEdge: floatPos.x + mytable.clientWidth - lcw - /*lastcell*/ mytable.rows[nr - 1].cells[nc - 1].clientWidth,
            left: floatPos.x,
            x: floatPos.x});
        setAtt(tlc.style, {
            height: tf.style.height,
            left: floatPos.x + 'px',
            top: floatPos.y + 'px',
            twidth: lcw + 2 + 'px'}
        );
        tf.rightEdge = tlc.rightEdge;
    }
    function setTableHeadGeometry() {
        setAtt(tf, {
            ybottom: floatPos.y + mytable.clientHeight - tf.clientHeight - /*last row*/ mytable.rows[nr - 1].clientHeight,
            tabtop: floatPos.y,
            x: floatPos.x,
            y: floatPos.y,
            left: floatPos.x,
            top: floatPos.y,
            right: tf.left + mytable.clientWidth,
            bottom: tf.top + mytable.clientHeight});
        setAtt(tf.style, {
            left: floatPos.x + 'px',
            top: floatPos.y + 'px',
            width: mytable.clientWidth + 'px'});
    }
    var mytable
            , row = [], floatPos
            , i, nc, nr, th, delta
            , k, tf, tlc = {}, lc = {}, lcw = 0;

    if (typeof head === 'undefined') {
        var head = {ncpth: [], nccol: 0};
    }

    mytable = document.getElementById(tableId);
    floatPos = absPos(mytable);
    tf = createDivHead(mytable, floatPos.x); // entire header

    if (typeof head === 'undefined') {
        var head = {ncpth: [], nccol: 0}; // default
    } else {
        tlc = createDivCornerHead(mytable, floatPos.x); // top left corener
        lc = createDivLeftColumn(mytable, floatPos.x); //  left column
        tlc.style.top = floatPos.y + 'px';
        tlc.x = 0;
    }
    //////////////////////////////
    /// header rows only
    /////////////////////////////
    document.body.appendChild(tf);
    tf.style.top = floatPos.y + 'px';
    nr = mytable.rows.length;
    if (head.nccol > 0) {
        document.body.appendChild(tlc);
    }
    for (k = 0; k < nr; k++) {
        if (mytable.rows[k].cells[0].tagName !== 'TH') {
            break;
        }
        row = mytable.rows[k];
        nc = row.cells.length;
        for (i = 0; i < nc; i++) { // copy content of header cells from table
            th = createHeaderCell(row.cells[i], row.cells[i].offsetTop, i);
            tf.appendChild(th);
            th.style.height = row.cells[i].clientHeight + 'px';
            if (k < head.ncpth.length && i < head.ncpth[k]) {// copy cells into top left corner div
                th = createHeaderCell(row.cells[i], row.cells[i].offsetTop, i);
                tlc.appendChild(th);
                th.style.height = row.cells[i].clientHeight + 'px';
            }
        }
    }
    tf.style.height = row.offsetTop + row.clientHeight + 'px';
    tf.rightEdge = 0;
    ///////////////////////
    //// left column cells  only
    ///////////////////////
    if (head.nccol > 0) {
        document.body.appendChild(lc);
        lcw = 0;
        delta = mytable.rows[head.ncpth.length].offsetTop;
        for (/*k from loop above*/; k < nr; k++) {
            row = mytable.rows[k];
            nc = row.cells.length;
            for (i = 0; i < head.nccol; i++) { // copy content of column cells from table
                th = createLeftColumn(row.cells[i], row.cells[i].offsetTop - delta, i, row.rowIndex);
                lc.appendChild(th);
            }
        }
        lcw = row.cells[head.nccol - 1].offsetLeft + row.cells[head.nccol - 1].clientWidth;
        setLeftColumnGeometry(head);
        lc.style.display = 'none';
        setTopLeftCornerGeometry();
        tlc.style.display = 'none';
    }
    setTableHeadGeometry();
    tf.style.display = 'none';

    function scroll() { // does the scrolling as event handler
        var y, x;
        y = window.pageYOffset;
        x = window.pageXOffset;
        /////////////////////////////////// vertical scrolling /////////////////////////
        if (tf.top <= y && y <= tf.ybottom) {
            if (tf.y !== y) {
                tlc.style.top = tf.style.top = y + 'px';
                tf.y = y;
                if (tf.style.display === 'none') {
                    tlc.style.display = tf.style.display = '';
                }
            } else if (tf.style.display === 'none') {
                tf.style.display = '';
            }
        } else if (y < tf.top || y > tf.bottom) {
            if (tf.style.display !== 'none') {
                tlc.style.display = tf.style.display = 'none';
                tlc.style.top = tf.style.top = tf.top + 'px';
            }
        }
        if (head.nccol === 0) {
            return;
        }
        /////////////////////////////// horizontal scrolling //////////////////
        if (tlc.left < x && x < tf.rightEdge) {
            if (tlc.x !== x) {
                lc.style.left = tlc.style.left = x + 'px';
                tlc.x = x;
                if (tlc.style.display === 'none') {
                    lc.style.display = tlc.style.display = '';
                }
            } else if (tlc.style.display === 'none') {
                tlc.style.display = '';
            }
        } else if (x <= tlc.left || x > tlc.right) {
            if (tlc.style.display !== 'none') {
                lc.style.display = tlc.style.display = 'none';
            }
        }
        return;
    }

    tf.sync = function(ri, what) { // method to force a new layout of pseudo header
        var mytable, nc, nr, k, i, j, l, th, row;
        mytable = document.getElementById(this.id.split('_')[1]);
        nr = mytable.rows.length;
        floatPos = absPos(mytable);
        for (k = 0, j = 0, l = 0; k < nr; k++) {
            if (mytable.rows[k].cells[0].tagName !== 'TH') {
                break;
            }
            row = mytable.rows[k];
            nc = row.cells.length;
            for (i = 0; i < nc; i++) { // copy content of header cells from table
                th = updateHeaderCell(tf.childNodes[j++], row.cells[i], row.cells[i].offsetTop, i);
                th.style.height = row.cells[i].clientHeight + 'px';
                if (k < head.ncpth.length && i < head.ncpth[k]) {// copy cells into top left corner div
                    th = updateHeaderCell(tlc.childNodes[l++], row.cells[i], row.cells[i].offsetTop, i);
                    th.style.height = row.cells[i].clientHeight + 'px';
                }
            }
        }
        if (head.nccol > 0) {
            this.row(ri, what);
            row = mytable.rows[head.ncpth.length];
            lcw = row.cells[head.nccol - 1].offsetLeft + row.cells[head.nccol - 1].clientWidth;
            setLeftColumnGeometry(head);
            setTopLeftCornerGeometry();
        }
        setTableHeadGeometry();
    };
    tf.row = function(ri, what) { // method to force a new layout of pseudo header
        var mytable, nc, k, row, j, i;
        mytable = document.getElementById(this.id.split('_')[1]);
        nr = mytable.rows.length;
        ri = ri * 1;
        what = what * 1;
        if (what === 1) {// insert
            row = mytable.rows[ri];
            nc = row.cells.length;
            for (i = 0; i < head.nccol; i++) { // copy content of column cells from table
                th = createLeftColumn(row.cells[i], row.cells[i].offsetTop - delta, i, ri);
                tf.lc.appendChild(th);
            }
        } else if (what === -1) {// delete
            // delete a pseudo row from left column
            for (k = 0; k < head.nccol; k++) {
                tf.lc.removeChild(tf.lc.childNodes[0]);
            }
        }
        //////////////////////////////
        /// brute force sync/rearange left columns
        /// //////////////////////////
        for (k = head.ncpth.length, j = 0; k < mytable.rows.length; k++) {
            row = mytable.rows[k];
            for (i = 0; i < head.nccol; i++) { // copy content of column cells from table
                updateLeftColumn(tf.lc.childNodes[j++], row.cells[i], row.cells[i].offsetTop - delta, i, row.rowIndex);
            }
        }
        return;
    };
    function addEvent(obj, ev, fu) {
        if (obj.addEventListener) {
            obj.addEventListener(ev, fu, false);
        } else {
            var eev = 'on' + ev;
            obj.attachEvent(eev, fu);
        }
    }
    addEvent(window, 'scroll', scroll);
    // pointers to corner and left column;
    tf.tlc = tlc;
    tf.lc = lc;
    return tf;
}<commit_msg>rewrote scroll function alternates now between fixed and absolute positions for headers and left columns depending on direction of scroll.<commit_after>/*************************************************************************
 float.js 1.0 Copyright (c) 2013 Heinrich Schweitzer
 Contact me at hgs@hgsweb.de
 This copyright notice MUST stay intact for use.

 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
 'Software'), to deal in the Software without restriction, including
 without limitation the rights to use, copy, modify, merge, publish,
 distribute, sublicense, and/or sell copies of the Software, and to
 permit persons to whom the Software is furnished to do so, subject to
 the following conditions:

 The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 ***************************************************************************/

/*//////////////////////////////////////////////////////////////
 Creates divs to mimic the table header.

 This header is allways displayed at the top of the table, if
 the original header is scrolled out of view.

 To use this funtion your table must have an id speficied.
 Once alle tables are rendered you call floatHeader(tableId,head),
 this returns the header object.

 /////////////////////////////////////////////////////////////*/

function floatHeader(tableId, head) {
    'use strict';
    function setAtt(s, o) {
        var opt;
        for (opt in o) {
            s[opt] = o[opt];
        }
        return s;
    }
    function absPos(obj) {// return absolute x,y position of obj
        var ob, pos = {};
        pos.x = obj.offsetLeft;
        pos.y = obj.offsetTop;
        ob = obj.offsetParent;
        while (ob.tagName !== 'BODY') {
            pos.x += ob.offsetLeft;
            pos.y += ob.offsetTop;
            ob = ob.offsetParent;
        }
        return pos;
    }
    function createDivHead(mytable, x) {
        var div = document.createElement('div');
        setAtt(div, {id: 'float_' + mytable.id,
            className: 'outerFloatHead',
            frozen: false,
            see: false}
        );
        setAtt(div.style, {zIndex: 15,
            width: mytable.clientWidth + 'px',
            left: x + 'px'
        }
        );
        return div;
    }
    function createDivCornerHead(mytable, x) {
        var div = document.createElement('div');
        setAtt(div, {id: 'floatcorner_' + mytable.id,
            className: 'floatCorner',
            frozen: false,
            see: false}
        );
        setAtt(div.style, {zIndex: 15,
            left: x + 'px',
            height: mytable.rows[0].cells[0].clientHeight + 'px'}
        );
        return div;
    }
    function createDivLeftColumn(mytable, x) {
        var div = document.createElement('div');
        setAtt(div, {id: 'floatleftcolumn_' + mytable.id,
            className: 'outerFloatHead',
            frozen: false,
            see: false}
        );
        setAtt(div.style, {zIndex: 12,
            left: x + 'px',
            height: mytable.rows[0].cells[0].clientHeight + 'px'}
        );
        return div;
    }
    function createHeaderCell(theCell, top, ci) {
        var div = document.createElement('div');
        updateHeaderCell(div, theCell, top, ci);
        return div;
    }
    function updateHeaderCell(div, theCell, top, ci) {
        setAtt(div, {className: 'floatHead ' + theCell.className,
            innerHTML: theCell.innerHTML,
            cellIndex: ci}
        );
        setAtt(div.style, {
            left: theCell.offsetLeft + 'px',
            top: top + 'px',
            width: theCell.clientWidth + 'px'
        }
        );
        return div;
    }
    function createLeftColumn(theCell, top, ci, ri) {
        var div = document.createElement('div');
        updateLeftColumn(div, theCell, top, ci, ri);
        return div;
    }
    function updateLeftColumn(div, theCell, top, ci, ri) {
        setAtt(div, {className: 'floatCol ' + theCell.className,
            innerHTML: theCell.innerHTML,
            cellIndex: ci,
            rowIndex: ri}
        );
        setAtt(div.style, {
            left: theCell.offsetLeft + 'px',
            top: top + 'px',
            width: theCell.clientWidth + 'px',
            height: theCell.clientHeight + 'px'}
        );
        return div;
    }
    function setLeftColumnGeometry(head) {
        setAtt(lc.style, {
            top: absPos(mytable.rows[head.ncpth.length].cells[0]).y + 'px',
            left: floatPos.x + 'px',
            height: mytable.clientHeight - mytable.rows[head.ncpth.length].offsetTop + 'px',
            width: lcw + 'px'});
        lc.top = parseInt(lc.style.top);
    }
    function setTopLeftCornerGeometry() {
        var nr, nc;
        nr = mytable.rows.length;
        nc = mytable.rows[nr - 1].cells.length;
        setAtt(tlc, {
            rightEdge: floatPos.x + mytable.clientWidth - lcw - /*lastcell*/ mytable.rows[nr - 1].cells[nc - 1].clientWidth,
            left: floatPos.x,
            x: floatPos.x});
        setAtt(tlc.style, {
            borderRight: '1px solid black',
            height: tf.style.height,
            left: floatPos.x + 'px',
            top: floatPos.y + 'px',
            width: lcw + 2 + 'px'}
        );
        tf.rightEdge = tlc.rightEdge;

    }
    function setTableHeadGeometry() {
        setAtt(tf, {
            ybottom: floatPos.y + mytable.clientHeight - tf.clientHeight - /*last row*/ mytable.rows[nr - 1].clientHeight,
            tabtop: floatPos.y,
            x: floatPos.x,
            y: floatPos.y,
            sx: 0,
            sy: 0,
            left: floatPos.x,
            top: floatPos.y,
            right: floatPos.x + mytable.clientWidth,
            bottom: floatPos.y + mytable.clientHeight});
        setAtt(tf.style, {
            left: floatPos.x + 'px',
            top: floatPos.y + 'px',
            width: mytable.clientWidth + 'px'});
    }
    var mytable
            , row = [], floatPos
            , i, nc, nr, th, delta
            , k, tf, tlc = {}, lc = {}, lcw = 0;
    mytable = document.getElementById(tableId);
    if (typeof head === 'undefined') {
        var head = {ncpth: [], nccol: 0};
        for (i = 0; i < mytable.rows.length; i++) {
            if (mytable.rows[i].cells[0].tagName !== 'TH') {
                break;
            }
            head.ncpth[i] = mytable.rows[i].cells.length;
        }
    }

    floatPos = absPos(mytable);
    tf = createDivHead(mytable, floatPos.x); // entire header

    if (typeof head === 'undefined') {
        var head = {ncpth: [], nccol: 0}; // default
    } else {
        tlc = createDivCornerHead(mytable, floatPos.x); // top left corener
        lc = createDivLeftColumn(mytable, floatPos.x); //  left column
        tlc.style.top = floatPos.y + 'px';
        tlc.x = 0;
    }
//////////////////////////////
/// header rows only
/////////////////////////////
    document.body.appendChild(tf);
    tf.style.top = floatPos.y + 'px';
    nr = mytable.rows.length;
    if (head.nccol > 0) {
        document.body.appendChild(tlc);
    }
    for (k = 0; k < nr; k++) {
        if (mytable.rows[k].cells[0].tagName !== 'TH') {
            break;
        }
        row = mytable.rows[k];
        nc = row.cells.length;
        for (i = 0; i < nc; i++) { // copy content of header cells from table
            th = createHeaderCell(row.cells[i], row.cells[i].offsetTop, i);
            tf.appendChild(th);
            th.style.height = row.cells[i].clientHeight + 'px';
            if (k < head.ncpth.length && i < head.ncpth[k]) {// copy cells into top left corner div
                th = createHeaderCell(row.cells[i], row.cells[i].offsetTop, i);
                tlc.appendChild(th);
                th.style.height = row.cells[i].clientHeight + 'px';
            }
        }
    }
    tf.style.height = row.offsetTop + row.clientHeight + 'px';
    tf.rightEdge = 0;
    ///////////////////////
    //// left column cells  only
    ///////////////////////
    if (head.nccol > 0) {
        document.body.appendChild(lc);
        lcw = 0;
        delta = mytable.rows[head.ncpth.length].offsetTop;
        for (/*k from loop above*/; k < nr; k++) {
            row = mytable.rows[k];
            nc = row.cells.length;
            for (i = 0; i < head.nccol; i++) { // copy content of column cells from table
                th = createLeftColumn(row.cells[i], row.cells[i].offsetTop - delta, i, row.rowIndex);
                lc.appendChild(th);
            }
        }
        lcw = row.cells[head.nccol - 1].offsetLeft + row.cells[head.nccol - 1].clientWidth;
        setLeftColumnGeometry(head);
        lc.style.display = 'none';
        setTopLeftCornerGeometry();
        tlc.style.display = 'none';
    }
    setTableHeadGeometry();
    tf.style.display = 'none';

    floatPos.yEdge = floatPos.y + mytable.clientHeight - tf.clientHeight - /*last row*/ mytable.rows[nr - 1].clientHeight;
    floatPos.xEdge = floatPos.x + mytable.clientWidth - lcw - /*lastcell*/ mytable.rows[nr - 1].cells[nc - 1].clientWidth;
    floatPos.right = floatPos.x + mytable.clientWidth;
    floatPos.bottom = floatPos.x + mytable.clientHeight;
    floatPos.ylc = absPos(mytable.rows[head.ncpth.length]).y;
    floatPos.sx = 0;
    floatPos.sy = 0;

    function scroll() {
        var y, x;
        y = window.pageYOffset;
        x = window.pageXOffset;

        function fixed(obj, x, y) {
            obj.style.left = x + 'px';
            obj.style.top = y + 'px';
            obj.style.position = 'fixed';
            if (obj.style.display === 'none') {
                obj.style.display = '';
                obj.see = true;
            }
            obj.frozen = true;
            return obj;
        }

        function absolute(obj, x, y) {
            obj.style.left = x + 'px';
            obj.style.top = y + 'px';
            obj.style.position = 'absolute';
            if (obj.style.display === 'none') {
                obj.style.display = '';
                obj.see = true;
            }
            obj.frozen = false;
            return obj;
        }
        function hide(obj) {
            if (obj.style.display !== 'none') {
                obj.style.display = 'none'
                obj.see = false;
                obj.frozen = false;
            }
            return obj;
        }
        if (floatPos.sy !== y) {
            floatPos.sy = y;
            if (lc.see && lc.frozen) {
                lc = absolute(lc, x, floatPos.y);
            }
            if (y < floatPos.y) {
                //hide all
                hide(tf);
                if (tlc.frozen && tlc.see) {
                    tlc = absolute(tlc, x, floatPos.y);
                }
            } else if (y < floatPos.yEdge) {
                // freeze tf
                if (!tf.frozen) {
                    tf = fixed(tf, floatPos.x - x, 0);
                }
                if (tlc.see) {
                    tlc = fixed(tlc, 0, 0);
                }
            } else if (y < floatPos.bottom) {
                if (tf.see) {
                    tf = absolute(tf, floatPos.x, y);
                }
                if (tlc.see) {
                    tlc = absolute(tlc, floatPos.x, y);
                }
            } else {
                hide(tf);
                hide(tlc);
                hide(lc);
            }
        }
        if (floatPos.sx !== x) {
            floatPos.sx = x;
            if (tf.see && tf.frozen) {
                tf = absolute(tf, floatPos.x, y);
            }
            if (x <= floatPos.x) {
                hide(lc);
                hide(tlc);
            } else if (x < floatPos.xEdge) {
                if (!tlc.see || !tlc.frozen) {
                    if (!tf.see) {
                        tlc = fixed(tlc, 0, floatPos.y - y);
                    } else {
                        tlc = fixed(tlc, 0, 0);
                    }
                    lc = fixed(lc, 0, floatPos.ylc - y);
                }
            } else if (x < floatPos.right) {

                if (tlc.frozen || tlc.view) {
                    tlc = absolute(tlc, x, parseInt(tlc.style.top) + y);
                    lc = absolute(lc, x, floatPos.ylc);
                }
            } else {/**
             hide(tf);
             hide(tlc);
             hide(lc);
             **/
            }
        }
    }
    tf.sync = function() {
        tf.syncRow(0, 0);
        scroll();
    };
    tf.syncRow = function(ri, what) { // method to force a new layout of pseudo header
        var mytable, nc, nr, k, i, j, l, th, row;
        mytable = document.getElementById(this.id.split('_')[1]);
        nr = mytable.rows.length;
        floatPos = absPos(mytable);
        for (k = 0, j = 0, l = 0; k < nr; k++) {
            if (mytable.rows[k].cells[0].tagName !== 'TH') {
                break;
            }
            row = mytable.rows[k];
            nc = row.cells.length;
            for (i = 0; i < nc; i++) { // copy content of header cells from table
                th = updateHeaderCell(tf.childNodes[j++], row.cells[i], row.cells[i].offsetTop, i);
                th.style.height = row.cells[i].clientHeight + 'px';
                if (k < head.ncpth.length && i < head.ncpth[k]) {// copy cells into top left corner div
                    th = updateHeaderCell(tlc.childNodes[l++], row.cells[i], row.cells[i].offsetTop, i);
                    th.style.height = row.cells[i].clientHeight + 'px';
                }
            }
        }
        if (head.nccol > 0) {
            if (what !== 99)
                this.row(ri, what);
            row = mytable.rows[head.ncpth.length];
            lcw = row.cells[head.nccol - 1].offsetLeft + row.cells[head.nccol - 1].clientWidth;
            setLeftColumnGeometry(head);
            setTopLeftCornerGeometry();
        }
        setTableHeadGeometry();

        floatPos.yEdge = floatPos.y + mytable.clientHeight - tf.clientHeight - /*last row*/ mytable.rows[nr - 1].clientHeight;
        floatPos.xEdge = floatPos.x + mytable.clientWidth - lcw - /*lastcell*/ mytable.rows[nr - 1].cells[nc - 1].clientWidth;
        floatPos.right = floatPos.x + mytable.clientWidth;
        floatPos.bottom = floatPos.x + mytable.clientHeight;
        floatPos.ylc = absPos(mytable.rows[head.ncpth.length]).y;
        floatPos.sx = 0;
        floatPos.sy = 0;

    };
    tf.row = function(ri, what) { // method to force a new layout of pseudo header
        var mytable, nc, k, row, j, i;
        mytable = document.getElementById(this.id.split('_')[1]);
        nr = mytable.rows.length;
        ri = ri * 1;
        what = what * 1;
        if (what === 1) {// insert
            row = mytable.rows[ri];
            nc = row.cells.length;
            for (i = 0; i < head.nccol; i++) { // copy content of column cells from table
                th = createLeftColumn(row.cells[i], row.cells[i].offsetTop - delta, i, ri);
                tf.lc.appendChild(th);
            }
        } else if (what === -1) {// delete
            // delete a pseudo row from left column
            for (k = 0; k < head.nccol; k++) {
                tf.lc.removeChild(tf.lc.childNodes[0]);
            }
        }
        //////////////////////////////
        /// brute force sync/rearange left columns
        /// //////////////////////////
        for (k = head.ncpth.length, j = 0; k < mytable.rows.length - 1; k++) {
            row = mytable.rows[k];
            for (i = 0; i < head.nccol; i++) { // copy content of column cells from table
                updateLeftColumn(tf.lc.childNodes[j++], row.cells[i], row.cells[i].offsetTop - delta, i, row.rowIndex);
            }
        }
        return;
    };
    function addEvent(obj, ev, fu) {
        if (obj.addEventListener) {
            obj.addEventListener(ev, fu, false);
        } else {
            var eev = 'on' + ev;
            obj.attachEvent(eev, fu);
        }
    }
    addEvent(window, 'scroll', scroll);
    // pointers to corner and left column;
    tf.tlc = tlc;
    tf.lc = lc;
    return tf;
}<|endoftext|>


<commit_before>/**
 * @license
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../../../styles/shared-styles.js';
import '../gr-change-list/gr-change-list.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-dialog/gr-dialog.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-commands-dialog/gr-create-commands-dialog.js';
import '../gr-create-change-help/gr-create-change-help.js';
import '../gr-create-destination-dialog/gr-create-destination-dialog.js';
import '../gr-user-header/gr-user-header.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-dashboard-view_html.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {appContext} from '../../../services/app-context.js';
import {changeIsOpen} from '../../../utils/change-util.js';

const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;

/**
 * @extends PolymerElement
 */
class GrDashboardView extends GestureEventListeners(
    LegacyElementMixin(PolymerElement)) {
  static get template() { return htmlTemplate; }

  static get is() { return 'gr-dashboard-view'; }
  /**
   * Fired when the title of the page should change.
   *
   * @event title-change
   */

  static get properties() {
    return {
      account: {
        type: Object,
        value: null,
      },
      preferences: Object,
      /** @type {{ selectedChangeIndex: number }} */
      viewState: Object,

      /** @type {{ project: string, user: string }} */
      params: {
        type: Object,
      },

      createChangeTap: {
        type: Function,
        value() {
          return e => this._createChangeTap(e);
        },
      },

      _results: Array,

      /**
       * For showing a "loading..." string during ajax requests.
       */
      _loading: {
        type: Boolean,
        value: true,
      },

      _showDraftsBanner: {
        type: Boolean,
        value: false,
      },

      _showNewUserHelp: {
        type: Boolean,
        value: false,
      },
    };
  }

  constructor() {
    super();
    this.reporting = appContext.reportingService;
  }

  static get observers() {
    return [
      '_paramsChanged(params.*)',
    ];
  }

  /** @override */
  attached() {
    super.attached();
    this._loadPreferences();
    this.addEventListener('reload', e => {
      e.stopPropagation();
      this._reload();
    });
  }

  _loadPreferences() {
    return this.$.restAPI.getLoggedIn().then(loggedIn => {
      if (loggedIn) {
        this.$.restAPI.getPreferences().then(preferences => {
          this.preferences = preferences;
        });
      } else {
        this.preferences = {};
      }
    });
  }

  _getProjectDashboard(project, dashboard) {
    const errFn = response => {
      this.dispatchEvent(new CustomEvent('page-error', {
        detail: {response},
        composed: true, bubbles: true,
      }));
    };
    return this.$.restAPI.getDashboard(
        project, dashboard, errFn).then(response => {
      if (!response) {
        return;
      }
      return {
        title: response.title,
        sections: response.sections.map(section => {
          const suffix = response.foreach ? ' ' + response.foreach : '';
          return {
            name: section.name,
            query: (section.query + suffix).replace(
                PROJECT_PLACEHOLDER_PATTERN, project),
          };
        }),
      };
    });
  }

  _computeTitle(user) {
    if (!user || user === 'self') {
      return 'My Reviews';
    }
    return 'Dashboard for ' + user;
  }

  _isViewActive(params) {
    return params.view === GerritNav.View.DASHBOARD;
  }

  _paramsChanged(paramsChangeRecord) {
    const params = paramsChangeRecord.base;

    if (!this._isViewActive(params)) {
      return Promise.resolve();
    }

    return this._reload();
  }

  /**
   * Reloads the element.
   *
   * @return {Promise<!Object>}
   */
  _reload() {
    this._loading = true;
    const {project, dashboard, title, user, sections} = this.params;
    const dashboardPromise = project ?
      this._getProjectDashboard(project, dashboard) :
      this.$.restAPI.getConfig().then(
          config => Promise.resolve(GerritNav.getUserDashboard(
              user,
              sections,
              title || this._computeTitle(user),
              config
          ))
      );

    const checkForNewUser = !project && user === 'self';
    return dashboardPromise
        .then(res => {
          if (res && res.title) {
            this.dispatchEvent(new CustomEvent('title-change', {
              detail: {title: res.title},
              composed: true, bubbles: true,
            }));
          }
          return this._fetchDashboardChanges(res, checkForNewUser);
        })
        .then(() => {
          this._maybeShowDraftsBanner();
          this.reporting.dashboardDisplayed();
        })
        .catch(err => {
          this.dispatchEvent(new CustomEvent('title-change', {
            detail: {
              title: title || this._computeTitle(user),
            },
            composed: true, bubbles: true,
          }));
          console.warn(err);
        })
        .then(() => { this._loading = false; });
  }

  /**
   * Fetches the changes for each dashboard section and sets this._results
   * with the response.
   *
   * @param {!Object} res
   * @param {boolean} checkForNewUser
   * @return {Promise}
   */
  _fetchDashboardChanges(res, checkForNewUser) {
    if (!res) { return Promise.resolve(); }

    let queries;

    if (window.PRELOADED_QUERIES
      && window.PRELOADED_QUERIES.dashboardQuery) {
      queries = window.PRELOADED_QUERIES.dashboardQuery;
      // we use preloaded query from index only on first page load
      window.PRELOADED_QUERIES.dashboardQuery = undefined;
    } else {
      queries = res.sections
          .map(section => (section.suffixForDashboard ?
            section.query + ' ' + section.suffixForDashboard :
            section.query));

      if (checkForNewUser) {
        queries.push('owner:self limit:1');
      }
    }

    return this.$.restAPI.getChanges(null, queries)
        .then(changes => {
          if (checkForNewUser) {
            // Last set of results is not meant for dashboard display.
            const lastResultSet = changes.pop();
            this._showNewUserHelp = lastResultSet.length == 0;
          }
          this._results = changes.map((results, i) => {
            return {
              name: res.sections[i].name,
              countLabel: this._computeSectionCountLabel(results),
              query: res.sections[i].query,
              results,
              isOutgoing: res.sections[i].isOutgoing,
            };
          }).filter((section, i) => i < res.sections.length && (
            !res.sections[i].hideIfEmpty ||
              section.results.length));
        });
  }

  _computeSectionCountLabel(changes) {
    if (!changes || !changes.length || changes.length == 0) {
      return '';
    }
    const more = changes[changes.length - 1]._more_changes;
    const numChanges = changes.length;
    const andMore = more ? ' and more' : '';
    return `(${numChanges}${andMore})`;
  }

  _computeUserHeaderClass(params) {
    if (!params || !!params.project || !params.user ||
        params.user === 'self') {
      return 'hide';
    }
    return '';
  }

  _handleToggleStar(e) {
    this.$.restAPI.saveChangeStarred(e.detail.change._number,
        e.detail.starred);
  }

  _handleToggleReviewed(e) {
    this.$.restAPI.saveChangeReviewed(e.detail.change._number,
        e.detail.reviewed);
  }

  /**
   * Banner is shown if a user is on their own dashboard and they have draft
   * comments on closed changes.
   */
  _maybeShowDraftsBanner() {
    this._showDraftsBanner = false;
    if (!(this.params.user === 'self')) { return; }

    const draftSection = this._results
        .find(section => section.query === 'has:draft');
    if (!draftSection || !draftSection.results.length) { return; }

    const closedChanges = draftSection.results
        .filter(change => !changeIsOpen(change));
    if (!closedChanges.length) { return; }

    this._showDraftsBanner = true;
  }

  _computeBannerClass(show) {
    return show ? '' : 'hide';
  }

  _handleOpenDeleteDialog() {
    this.$.confirmDeleteOverlay.open();
  }

  _handleConfirmDelete() {
    this.$.confirmDeleteDialog.disabled = true;
    return this.$.restAPI.deleteDraftComments('-is:open').then(() => {
      this._closeConfirmDeleteOverlay();
      this._reload();
    });
  }

  _closeConfirmDeleteOverlay() {
    this.$.confirmDeleteOverlay.close();
  }

  _computeDraftsLink() {
    return GerritNav.getUrlForSearchQuery('has:draft -is:open');
  }

  _createChangeTap(e) {
    this.$.destinationDialog.open();
  }

  _handleDestinationConfirm(e) {
    this.$.commandsDialog.branch = e.detail.branch;
    this.$.commandsDialog.open();
  }
}

customElements.define(GrDashboardView.is, GrDashboardView);
<commit_msg>Change the sorting of changes in the 'Your Turn' dashboard section<commit_after>/**
 * @license
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../../../styles/shared-styles.js';
import '../gr-change-list/gr-change-list.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-dialog/gr-dialog.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-commands-dialog/gr-create-commands-dialog.js';
import '../gr-create-change-help/gr-create-change-help.js';
import '../gr-create-destination-dialog/gr-create-destination-dialog.js';
import '../gr-user-header/gr-user-header.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-dashboard-view_html.js';
import {GerritNav, YOUR_TURN} from '../../core/gr-navigation/gr-navigation.js';
import {appContext} from '../../../services/app-context.js';
import {changeIsOpen} from '../../../utils/change-util.js';
import {parseDate} from '../../../utils/date-util.js';

const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;

/**
 * @extends PolymerElement
 */
class GrDashboardView extends GestureEventListeners(
    LegacyElementMixin(PolymerElement)) {
  static get template() { return htmlTemplate; }

  static get is() { return 'gr-dashboard-view'; }
  /**
   * Fired when the title of the page should change.
   *
   * @event title-change
   */

  static get properties() {
    return {
      account: {
        type: Object,
        value: null,
      },
      preferences: Object,
      /** @type {{ selectedChangeIndex: number }} */
      viewState: Object,

      /** @type {{ project: string, user: string }} */
      params: {
        type: Object,
      },

      createChangeTap: {
        type: Function,
        value() {
          return e => this._createChangeTap(e);
        },
      },

      _results: Array,

      /**
       * For showing a "loading..." string during ajax requests.
       */
      _loading: {
        type: Boolean,
        value: true,
      },

      _showDraftsBanner: {
        type: Boolean,
        value: false,
      },

      _showNewUserHelp: {
        type: Boolean,
        value: false,
      },
    };
  }

  constructor() {
    super();
    this.reporting = appContext.reportingService;
  }

  static get observers() {
    return [
      '_paramsChanged(params.*)',
    ];
  }

  /** @override */
  attached() {
    super.attached();
    this._loadPreferences();
    this.addEventListener('reload', e => {
      e.stopPropagation();
      this._reload();
    });
  }

  _loadPreferences() {
    return this.$.restAPI.getLoggedIn().then(loggedIn => {
      if (loggedIn) {
        this.$.restAPI.getPreferences().then(preferences => {
          this.preferences = preferences;
        });
      } else {
        this.preferences = {};
      }
    });
  }

  _getProjectDashboard(project, dashboard) {
    const errFn = response => {
      this.dispatchEvent(new CustomEvent('page-error', {
        detail: {response},
        composed: true, bubbles: true,
      }));
    };
    return this.$.restAPI.getDashboard(
        project, dashboard, errFn).then(response => {
      if (!response) {
        return;
      }
      return {
        title: response.title,
        sections: response.sections.map(section => {
          const suffix = response.foreach ? ' ' + response.foreach : '';
          return {
            name: section.name,
            query: (section.query + suffix).replace(
                PROJECT_PLACEHOLDER_PATTERN, project),
          };
        }),
      };
    });
  }

  _computeTitle(user) {
    if (!user || user === 'self') {
      return 'My Reviews';
    }
    return 'Dashboard for ' + user;
  }

  _isViewActive(params) {
    return params.view === GerritNav.View.DASHBOARD;
  }

  _paramsChanged(paramsChangeRecord) {
    const params = paramsChangeRecord.base;

    if (!this._isViewActive(params)) {
      return Promise.resolve();
    }

    return this._reload();
  }

  /**
   * Reloads the element.
   *
   * @return {Promise<!Object>}
   */
  _reload() {
    this._loading = true;
    const {project, dashboard, title, user, sections} = this.params;
    const dashboardPromise = project ?
      this._getProjectDashboard(project, dashboard) :
      this.$.restAPI.getConfig().then(
          config => Promise.resolve(GerritNav.getUserDashboard(
              user,
              sections,
              title || this._computeTitle(user),
              config
          ))
      );

    const checkForNewUser = !project && user === 'self';
    return dashboardPromise
        .then(res => {
          if (res && res.title) {
            this.dispatchEvent(new CustomEvent('title-change', {
              detail: {title: res.title},
              composed: true, bubbles: true,
            }));
          }
          return this._fetchDashboardChanges(res, checkForNewUser);
        })
        .then(() => {
          this._maybeShowDraftsBanner();
          this.reporting.dashboardDisplayed();
        })
        .catch(err => {
          this.dispatchEvent(new CustomEvent('title-change', {
            detail: {
              title: title || this._computeTitle(user),
            },
            composed: true, bubbles: true,
          }));
          console.warn(err);
        })
        .then(() => { this._loading = false; });
  }

  /**
   * Fetches the changes for each dashboard section and sets this._results
   * with the response.
   *
   * @param {!Object} res
   * @param {boolean} checkForNewUser
   * @return {Promise}
   */
  _fetchDashboardChanges(res, checkForNewUser) {
    if (!res) { return Promise.resolve(); }

    let queries;

    if (window.PRELOADED_QUERIES
      && window.PRELOADED_QUERIES.dashboardQuery) {
      queries = window.PRELOADED_QUERIES.dashboardQuery;
      // we use preloaded query from index only on first page load
      window.PRELOADED_QUERIES.dashboardQuery = undefined;
    } else {
      queries = res.sections
          .map(section => (section.suffixForDashboard ?
            section.query + ' ' + section.suffixForDashboard :
            section.query));

      if (checkForNewUser) {
        queries.push('owner:self limit:1');
      }
    }

    return this.$.restAPI.getChanges(null, queries)
        .then(changes => {
          if (checkForNewUser) {
            // Last set of results is not meant for dashboard display.
            const lastResultSet = changes.pop();
            this._showNewUserHelp = lastResultSet.length == 0;
          }
          this._results = changes.map((results, i) => {
            return {
              name: res.sections[i].name,
              countLabel: this._computeSectionCountLabel(results),
              query: res.sections[i].query,
              results: this._maybeSortResults(res.sections[i].name, results),
              isOutgoing: res.sections[i].isOutgoing,
            };
          }).filter((section, i) => i < res.sections.length && (
            !res.sections[i].hideIfEmpty ||
              section.results.length));
        });
  }

  /**
   * Usually we really want to stick to the sorting that the backend provides,
   * but for the "Your Turn" section it is important to put the changes at the
   * top where the current user is a reviewer. Owned changes are less important.
   * And then we want to emphasize the changes where the waiting time is larger.
   */
  _maybeSortResults(name, results) {
    const userId = this.account && this.account._account_id;
    const sortedResults = [...results];
    if (name === YOUR_TURN.name && userId) {
      sortedResults.sort((c1, c2) => {
        const c1Owner = c1.owner._account_id === userId;
        const c2Owner = c2.owner._account_id === userId;
        if (c1Owner !== c2Owner) return c1Owner ? 1 : -1;
        // Should never happen, because the change is in the 'Your Turn'
        // section, so the userId should be found in the attention set of both.
        if (!c1.attention_set || !c1.attention_set[userId]) return 0;
        if (!c2.attention_set || !c2.attention_set[userId]) return 0;
        const c1Update = c1.attention_set[userId].last_update;
        const c2Update = c2.attention_set[userId].last_update;
        // Should never happen that an attention set entry has no update.
        if (!c1Update || !c2Update) return c1Update ? 1 : -1;
        return parseDate(c1Update) - parseDate(c2Update);
      });
    }
    return sortedResults;
  }

  _computeSectionCountLabel(changes) {
    if (!changes || !changes.length || changes.length == 0) {
      return '';
    }
    const more = changes[changes.length - 1]._more_changes;
    const numChanges = changes.length;
    const andMore = more ? ' and more' : '';
    return `(${numChanges}${andMore})`;
  }

  _computeUserHeaderClass(params) {
    if (!params || !!params.project || !params.user ||
        params.user === 'self') {
      return 'hide';
    }
    return '';
  }

  _handleToggleStar(e) {
    this.$.restAPI.saveChangeStarred(e.detail.change._number,
        e.detail.starred);
  }

  _handleToggleReviewed(e) {
    this.$.restAPI.saveChangeReviewed(e.detail.change._number,
        e.detail.reviewed);
  }

  /**
   * Banner is shown if a user is on their own dashboard and they have draft
   * comments on closed changes.
   */
  _maybeShowDraftsBanner() {
    this._showDraftsBanner = false;
    if (!(this.params.user === 'self')) { return; }

    const draftSection = this._results
        .find(section => section.query === 'has:draft');
    if (!draftSection || !draftSection.results.length) { return; }

    const closedChanges = draftSection.results
        .filter(change => !changeIsOpen(change));
    if (!closedChanges.length) { return; }

    this._showDraftsBanner = true;
  }

  _computeBannerClass(show) {
    return show ? '' : 'hide';
  }

  _handleOpenDeleteDialog() {
    this.$.confirmDeleteOverlay.open();
  }

  _handleConfirmDelete() {
    this.$.confirmDeleteDialog.disabled = true;
    return this.$.restAPI.deleteDraftComments('-is:open').then(() => {
      this._closeConfirmDeleteOverlay();
      this._reload();
    });
  }

  _closeConfirmDeleteOverlay() {
    this.$.confirmDeleteOverlay.close();
  }

  _computeDraftsLink() {
    return GerritNav.getUrlForSearchQuery('has:draft -is:open');
  }

  _createChangeTap(e) {
    this.$.destinationDialog.open();
  }

  _handleDestinationConfirm(e) {
    this.$.commandsDialog.branch = e.detail.branch;
    this.$.commandsDialog.open();
  }
}

customElements.define(GrDashboardView.is, GrDashboardView);
<|endoftext|>


<commit_before>/* eslint-env jest */

import React from 'react'
import { mount, shallow } from 'enzyme'
import SetUsernameMutation, {
  __runOnCompleted,
} from 'js/mutations/SetUsernameMutation'
import UsernameField from 'js/components/General/UsernameField'
import { checkIfEmailVerified } from 'js/authentication/helpers'
import TextField from '@material-ui/core/TextField'

jest.mock('js/mutations/SetUsernameMutation')
jest.mock('js/authentication/helpers')
jest.mock('js/authentication/user')
jest.mock('js/navigation/navigation')

const getMockProps = () => ({
  app: 'tab',
  user: {
    id: 'abc-123',
  },
})

afterEach(() => {
  jest.clearAllMocks()
})

describe('EnterUsernameForm tests', () => {
  it('renders without error', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    shallow(<EnterUsernameForm {...mockProps} />)
  })

  it('calls checkIfEmailVerified on mount', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    shallow(<EnterUsernameForm {...mockProps} />)
    expect(checkIfEmailVerified).toHaveBeenCalled()
  })

  it('calls SetUsernameMutation when entering a username', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )

    // Enter a username
    // https://github.com/airbnb/enzyme/issues/76#issuecomment-259734821
    usernameTextField.instance().value = 'bobert'

    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')
    expect(SetUsernameMutation).toHaveBeenCalled()
  })

  it('it does not call SetUsernameMutation when the username is too short and instead shows an error message', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )

    // Enter a too-short username
    usernameTextField.instance().value = 'x'
    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')

    // We shouldn't call to save the username
    expect(SetUsernameMutation).not.toHaveBeenCalled()

    const textFieldComp = wrapper
      .find('[data-test-id="enter-username-form-username-field"]')
      .find(TextField)
    expect(textFieldComp.prop('helperText')).toEqual(
      'Must be at least two characters.'
    )
    expect(textFieldComp.prop('error')).toBe(true)
  })

  it('it shows an error message when the username is a duplicate', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)

    // Enter a username
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )
    usernameTextField.instance().value = 'Sunol'
    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')

    // Mock a response with a duplicate username error
    __runOnCompleted({
      setUsername: {
        user: null,
        errors: [
          {
            code: 'USERNAME_DUPLICATE',
            message: 'Username already exists',
          },
        ],
      },
    })
    wrapper.update()

    const textFieldComp = wrapper
      .find('[data-test-id="enter-username-form-username-field"]')
      .find(TextField)
    expect(textFieldComp.prop('helperText')).toEqual(
      'Username is already taken. Please choose another.'
    )
    expect(textFieldComp.prop('error')).toBe(true)
  })

  it('has an input field label of "Username for Tab for a Cause" when the "app" prop === "tab"', () => {
    const mockProps = getMockProps()
    mockProps.app = 'tab'
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const wrapper = shallow(<EnterUsernameForm {...mockProps} />)
    expect(wrapper.find(UsernameField).prop('label')).toEqual(
      'Username for Tab for a Cause'
    )
  })

  it('has an input field label of "Username for Search for a Cause" when the "app" prop === "tab"', () => {
    const mockProps = getMockProps()
    mockProps.app = 'search'
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const wrapper = shallow(<EnterUsernameForm {...mockProps} />)
    expect(wrapper.find(UsernameField).prop('label')).toEqual(
      'Username for Search for a Cause'
    )
  })
})
<commit_msg>Add tests<commit_after>/* eslint-env jest */

import React from 'react'
import { mount, shallow } from 'enzyme'
import SetUsernameMutation, {
  __runOnCompleted,
} from 'js/mutations/SetUsernameMutation'
import UsernameField from 'js/components/General/UsernameField'
import { checkIfEmailVerified } from 'js/authentication/helpers'
import TextField from '@material-ui/core/TextField'
import { goTo } from 'js/navigation/navigation'
import { setUsernameInLocalStorage } from 'js/authentication/user'

jest.mock('js/mutations/SetUsernameMutation')
jest.mock('js/authentication/helpers')
jest.mock('js/authentication/user')
jest.mock('js/navigation/navigation')
jest.mock('js/authentication/user')

const getMockProps = () => ({
  app: 'tab',
  user: {
    id: 'abc-123',
  },
})

afterEach(() => {
  jest.clearAllMocks()
})

describe('EnterUsernameForm tests', () => {
  it('renders without error', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    shallow(<EnterUsernameForm {...mockProps} />)
  })

  it('calls checkIfEmailVerified on mount', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    shallow(<EnterUsernameForm {...mockProps} />)
    expect(checkIfEmailVerified).toHaveBeenCalled()
  })

  it('calls SetUsernameMutation when entering a username', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )

    // Enter a username
    // https://github.com/airbnb/enzyme/issues/76#issuecomment-259734821
    usernameTextField.instance().value = 'bobert'

    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')
    expect(SetUsernameMutation).toHaveBeenCalled()
  })

  it('does not call SetUsernameMutation when the username is too short and instead shows an error message', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )

    // Enter a too-short username
    usernameTextField.instance().value = 'x'
    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')

    // We shouldn't call to save the username
    expect(SetUsernameMutation).not.toHaveBeenCalled()

    const textFieldComp = wrapper
      .find('[data-test-id="enter-username-form-username-field"]')
      .find(TextField)
    expect(textFieldComp.prop('helperText')).toEqual(
      'Must be at least two characters.'
    )
    expect(textFieldComp.prop('error')).toBe(true)
  })

  it('shows an error message when the username is a duplicate', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)

    // Enter a username
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )
    usernameTextField.instance().value = 'Sunol'
    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')

    // Mock a response with a duplicate username error
    __runOnCompleted({
      setUsername: {
        user: null,
        errors: [
          {
            code: 'USERNAME_DUPLICATE',
            message: 'Username already exists',
          },
        ],
      },
    })
    wrapper.update()

    const textFieldComp = wrapper
      .find('[data-test-id="enter-username-form-username-field"]')
      .find(TextField)
    expect(textFieldComp.prop('helperText')).toEqual(
      'Username is already taken. Please choose another.'
    )
    expect(textFieldComp.prop('error')).toBe(true)
  })

  it('shows a generic error message when request fails for an unexpected reason', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)

    // Enter a username
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )
    usernameTextField.instance().value = 'Sunol'
    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')

    // Mock a response with a duplicate username error
    __runOnCompleted({
      setUsername: {
        user: null,
        errors: [
          {
            problem: 'UNEXPECTED_THING',
          },
        ],
      },
    })
    wrapper.update()

    const textFieldComp = wrapper
      .find('[data-test-id="enter-username-form-username-field"]')
      .find(TextField)
    expect(textFieldComp.prop('helperText')).toEqual(
      'There was an error saving your username. Please try again later.'
    )
    expect(textFieldComp.prop('error')).toBe(true)
  })

  it('has an input field label of "Username for Tab for a Cause" when the "app" prop === "tab"', () => {
    const mockProps = getMockProps()
    mockProps.app = 'tab'
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const wrapper = shallow(<EnterUsernameForm {...mockProps} />)
    expect(wrapper.find(UsernameField).prop('label')).toEqual(
      'Username for Tab for a Cause'
    )
  })

  it('has an input field label of "Username for Search for a Cause" when the "app" prop === "tab"', () => {
    const mockProps = getMockProps()
    mockProps.app = 'search'
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const wrapper = shallow(<EnterUsernameForm {...mockProps} />)
    expect(wrapper.find(UsernameField).prop('label')).toEqual(
      'Username for Search for a Cause'
    )
  })

  it('saves the username to localStorage when successfully saved server-side ', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)

    // Enter a username
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )
    usernameTextField.instance().value = 'Sunol'
    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')
    __runOnCompleted({
      setUsername: {
        user: {
          username: 'AirplaneLover',
        },
        errors: null,
      },
    })

    expect(setUsernameInLocalStorage).toHaveBeenCalledWith('AirplaneLover')
  })

  it('redirects to the app after saving the username', () => {
    const EnterUsernameForm = require('js/components/Authentication/EnterUsernameForm')
      .default
    const mockProps = getMockProps()
    const wrapper = mount(<EnterUsernameForm {...mockProps} />)

    // Enter a username
    const usernameTextField = wrapper.find(
      '[data-test-id="enter-username-form-username-field"] input'
    )
    usernameTextField.instance().value = 'Sunol'
    const button = wrapper
      .find('[data-test-id="enter-username-form-button"]')
      .first()
    button.simulate('click')
    __runOnCompleted({
      setUsername: {
        user: {
          username: 'Sunol',
        },
        errors: null,
      },
    })

    // TODO: this should be app-specific
    expect(goTo).toHaveBeenCalledWith('/newtab/')
  })
})
<|endoftext|>


<commit_before>var Backbone   = require('backbone'),
    $          = require('jquery'),
    _          = require('underscore');

function navigate(categories, keyWords) {
  var route  = 'query?categories=' + categories.join(',') + '&search=' + encodeURIComponent(keyWords),
      router = require('routers/router').instance;

  router.navigate(route, { trigger: true });
}

var IndexView = Backbone.View.extend({
  template: require('templates/index'),

  events: {
    'click .search-button': 'submit',
    'submit #searchForm': 'submit',
    'click ul.categories .btn': 'toggleCheckbox'
  },

  render: function() {
    this.$el.html(this.template({}));
    return this;
  },

  toggleCheckbox: function(event) {
    $(event.target).find('.icon-ok').toggle();
    return false;
  },

  submit: function(event) {
    var category,
        categories   = [],
        keyWords     = this.$('#search_name').val(),
        visibleIcons = this.$('.category .icon-ok:visible');

    _.each(visibleIcons, function(icon) {
      category = $(icon).closest('button').data('category');
      categories.push(category);
    });

    if (categories.length === 0 && keyWords === '') { return; }
    navigate(categories, keyWords);
    return false;
  }
});

module.exports = IndexView;
<commit_msg>fixed filtering on / broken by button->div change in category template<commit_after>var Backbone   = require('backbone'),
    $          = require('jquery'),
    _          = require('underscore');

function navigate(categories, keyWords) {
  var route  = 'query?categories=' + categories.join(',') + '&search=' + encodeURIComponent(keyWords),
      router = require('routers/router').instance;

  router.navigate(route, { trigger: true });
}

var IndexView = Backbone.View.extend({
  template: require('templates/index'),

  events: {
    'click .search-button': 'submit',
    'submit #searchForm': 'submit',
    'click ul.categories .btn': 'toggleCheckbox'
  },

  render: function() {
    this.$el.html(this.template({}));
    return this;
  },

  toggleCheckbox: function(event) {
    $(event.target).find('.icon-ok').toggle();
    return false;
  },

  submit: function(event) {
    var category,
        categories   = [],
        keyWords     = this.$('#search_name').val(),
        visibleIcons = this.$('.category .icon-ok:visible');

    _.each(visibleIcons, function(icon) {
      category = $(icon).closest('.category').data('category');
      categories.push(category);
    });

    if (categories.length === 0 && keyWords === '') { return; }
    navigate(categories, keyWords);
    return false;
  }
});

module.exports = IndexView;
<|endoftext|>


<commit_before>let floatQueue = [];
let floatData = {};
let floatTimer;
let expressionTimer;
let steamListingInfo = {};
let listingInfoPromises = [];
let validExpressionVars = ['float', 'seed', 'minfloat', 'maxfloat'];
let filterStrings = [];
let filters = [];

// retrieve g_rgListingInfo from page script
window.addEventListener('message', (e) => {
    if (e.data.type == 'listingInfo') {
        steamListingInfo = e.data.listingInfo;

        // resolve listingInfoPromises
        for (let promise of listingInfoPromises) promise(steamListingInfo);

        listingInfoPromises = [];
    }
});

const retrieveListingInfoFromPage = function(listingId) {
    if (listingId != null && (listingId in steamListingInfo)) {
        return Promise.resolve(steamListingInfo);
    }

    window.postMessage({
        type: 'requestListingInfo'
    }, '*');

    return new Promise((resolve) => {
        listingInfoPromises.push(resolve);
    });
};

const getFloatData = function(listingId, inspectLink) {
    if (listingId in floatData) {
        return Promise.resolve({ iteminfo: floatData[listingId] });
    }

    return fetch(`https://api.csgofloat.com:1738/?url=${inspectLink}`)
    .then((response) => {
        if (response.ok) return response.json();
        return response.json().then((err) => { throw err; });
    });
};

const showFloat = function(listingId) {
    let itemInfo = floatData[listingId];

    let floatDiv = document.querySelector(`#item_${listingId}_floatdiv`);

    if (floatDiv) {
        // Remove the "get float" button
        let floatButton = floatDiv.querySelector('.float-btn');
        if (floatButton) floatDiv.removeChild(floatButton);

        // Remove message div
        let msgdiv = floatDiv.querySelector('.floatmessage');
        if (msgdiv) floatDiv.removeChild(msgdiv);

        // Add the float value
        let itemFloatDiv = floatDiv.querySelector('.itemfloat');
        if (itemFloatDiv) itemFloatDiv.innerText = `Float: ${itemInfo.floatvalue}`;

        // Add the paint seed
        let seedDiv = floatDiv.querySelector('.itemseed');
        if (seedDiv) seedDiv.innerText = `Paint Seed: ${itemInfo.paintseed}`;

        let vars = {
            'float': itemInfo.floatvalue,
            'seed': itemInfo.paintseed,
            'minfloat': itemInfo.min,
            'maxfloat': itemInfo.max
        };

        // Check to see if there is a filter match
        for (let filter of filters) {
            if (filter(vars) === 1) {
                // highlight the div
                floatDiv.parentNode.parentNode.style.backgroundColor = 'rgb(53, 73, 8)';
                break;
            }
        }
    }
};

const processFloatQueue = function() {
    if (floatQueue.length === 0) { return setTimeout(processFloatQueue, 100); }

    let lastItem = floatQueue.shift();

    let floatDiv = document.querySelector(`#item_${lastItem.listingId}_floatdiv`);

    if (!floatDiv) {
        // they have switched pages since initiating the request, so continue
        processFloatQueue();
        return;
    }

    let buttonText = floatDiv.querySelector('span');

    if (buttonText) buttonText.innerText = 'Fetching';

    getFloatData(lastItem.listingId, lastItem.inspectLink)
    .then((data) => {
        floatData[lastItem.listingId] = data.iteminfo;

        showFloat(lastItem.listingId);

        processFloatQueue();
    })
    .catch((err) => {
        // Reset the button text for this itemid
        if (buttonText) buttonText.innerText = 'Get Float';

        // Change the message div for this item to the error
        if (floatDiv) {
            floatDiv.querySelector('.floatmessage').innerText = err.error || 'Unknown Error';
        }

        processFloatQueue();
    });
};

// Puts all of the available items on the page into the queue for float retrieval
const getAllFloats = function() {
    retrieveListingInfoFromPage()
    .then((steamListingData) => {
        // Get all current items on the page (in proper order)
        let listingRows = document.querySelectorAll('.market_listing_row.market_recent_listing_row');

        for (let row of listingRows) {
            let id = row.id.replace('listing_', '');

            let listingData = steamListingData[id];

            let inspectLink = listingData.asset.market_actions[0].link
            .replace('%listingid%', id)
            .replace('%assetid%', listingData.asset.id);

            floatQueue.push({ listingId: id, inspectLink: inspectLink });
        }
    });
};

const addFilter = function () {
    let filter = document.querySelector('#float_expression_filter').value;

    try {
        let compiled = compileExpression(filter, {}, validExpressionVars);

        // We know it is a valid expression
        filterStrings.push(filter);
        filters.push(compiled);

        addFilterUI(filter);
        saveFilters();
    }
    catch (e) {
        return;
    }
};

const removeFilter = function(e) {
    let removeBtn = e.srcElement;

    // get the parent
    let thisFilterDiv = removeBtn.parentNode.parentNode;

    // Remove the button
    thisFilterDiv.removeChild(removeBtn.parentNode);

    // get the expression string
    let expression = thisFilterDiv.innerText;

    // remove the div
    document.querySelector('#floatFilters').removeChild(thisFilterDiv);

    // Remove it from the arrays
    let filterID = filterStrings.indexOf(expression.trim());

    if (filterID === -1) return;

    filterStrings.splice(filterID, 1);
    filters.splice(filterID, 1);

    saveFilters();
};

const filterKeyPress = function() {
    if (expressionTimer) clearTimeout(expressionTimer);

    expressionTimer = setTimeout(() => {
        let input = document.querySelector('#float_expression_filter');
        let compileError = document.querySelector('#compileError');
        let status = document.querySelector('#compileStatus');
        let addFilterBtn = document.querySelector('#addFloatFilter');

        let expression = input.value;

        // try to compile the expression
        try {
            compileExpression(expression, {}, validExpressionVars);
            status.setAttribute('error', 'false');
            status.innerText = '✓';
            compileError.innerText = '';
            addFilterBtn.style.display = '';
        }
        catch (e) {
            status.setAttribute('error', 'true');
            status.innerText = '✗';
            compileError.innerText = e.message;
            addFilterBtn.style.display = 'none';
        }
    }, 250);
};

const getSaveKey = function() {
    let itemname = document.querySelector('.market_listing_nav a:nth-child(2)');

    if (!itemname) return;
    else return itemname.innerText + '_expressions';
};

const getSavedFilters = function(cb) {
    let key = getSaveKey();

    if (!key) cb([]);

    let syncFilters = {};
    syncFilters[key] = [];

    chrome.storage.sync.get(syncFilters, (items) => {
        cb(items[key]);
    });
};

const saveFilters = function() {
    let key = getSaveKey();

    if (!key) return;

    let syncFilters = {};
    syncFilters[key] = filterStrings;

    chrome.storage.sync.set(syncFilters);
};

const addFilterUI = function(expression) {
    let parentDiv = document.querySelector('#floatFilters');

    let thisDiv = document.createElement('div');
    thisDiv.innerText = expression;

    // Add remove filter btn
    let removeFilterBtn = createButton('Remove Filter', removeFilter, 'grey');
    removeFilterBtn.style.marginTop = '-3px';
    removeFilterBtn.style.float = 'right';
    thisDiv.appendChild(removeFilterBtn);

    // Add line break
    let hr = document.createElement('hr');
    thisDiv.appendChild(hr);

    parentDiv.appendChild(thisDiv);
};

const createButton = function(text, eventListener, colour) {
    let btn = document.createElement('a');
    btn.classList.add(`btn_${colour}_white_innerfade`);
    btn.classList.add('btn_small');
    btn.classList.add('float-btn');
    btn.addEventListener('click', eventListener);

    let span = document.createElement('span');
    span.innerText = text;
    btn.appendChild(span);

    return btn;
};

const addFiltersDiv = function(parent) {
    let filterdiv = document.createElement('div');
    filterdiv.id = 'floatFilter';
    parent.appendChild(filterdiv);

    // Add separator
    let hr = document.createElement('hr');
    filterdiv.appendChild(hr);

    // Adds filters div
    let filtersdiv = document.createElement('div');
    filtersdiv.id = 'floatFilters';
    filterdiv.appendChild(filtersdiv);

    // Add new filter input box
    let input = document.createElement('input');
    input.id = 'float_expression_filter';
    input.classList.add('filter_search_box');
    input.placeholder = 'Add Float Highlight Filter';
    input.style.width = '350px';
    input.addEventListener('keyup', filterKeyPress);
    filterdiv.appendChild(input);

    // Add compile status indicator
    let status = document.createElement('div');
    status.id = 'compileStatus';
    filterdiv.appendChild(status);

    // Add new filter btn
    let addFilterBtn = createButton('Add Filter', addFilter, 'green');
    addFilterBtn.id = 'addFloatFilter';
    addFilterBtn.style.display = 'none';
    addFilterBtn.style.marginLeft = '10px';

    filterdiv.appendChild(addFilterBtn);

    // Compile error div
    let compileError = document.createElement('div');
    compileError.id = 'compileError';
    filterdiv.appendChild(compileError);

    // Add any saved filters
    getSavedFilters((expressions) => {
        for (let expression of expressions) {
            filterStrings.push(expression);
            filters.push(compileExpression(expression, {}, validExpressionVars));
            addFilterUI(expression);
        }
    });
};

// Adds float utilities
const addFloatUtilities = function() {
    let parentDiv = document.createElement('div');
    parentDiv.id = 'floatUtilities';

    // Add get all floats button
    let allFloatButton = createButton('Get All Floats', getAllFloats, 'green');
    parentDiv.appendChild(allFloatButton);

    // Add github link
    let githubLink = document.createElement('a');
    githubLink.classList.add('float-github');
    githubLink.href = 'https://github.com/Step7750/CSGOFloat';
    githubLink.innerText = 'Powered by CSGOFloat';
    parentDiv.appendChild(githubLink);

    // Add filter div
    addFiltersDiv(parentDiv);

    document.querySelector('#searchResultsTable').insertBefore(parentDiv, document.querySelector('#searchResultsRows'));
};

const getFloatButtonClicked = function(e) {
    let row = e.currentTarget.parentElement.parentElement.parentElement;
    let id = row.id.replace('listing_', '');

    retrieveListingInfoFromPage(id)
    .then((steamListingData) => {
        let listingData = steamListingData[id];

        if (!listingData) return;

        let inspectLink = listingData.asset.market_actions[0].link
        .replace('%listingid%', id)
        .replace('%assetid%', listingData.asset.id);

        floatQueue.push({ listingId: id, inspectLink: inspectLink });
    });
};

// If an item on the current page doesn't have the float div/buttons, this function adds it
const addButtons = function() {
    // Iterate through each item on the page
    let listingRows = document.querySelectorAll('.market_listing_row.market_recent_listing_row');

    for (let row of listingRows) {
        let id = row.id.replace('listing_', '');

        if (row.querySelector(`#item_${id}_floatdiv`)) { continue; }

        let listingNameElement = row.querySelector(`#listing_${id}_name`);

        let floatDiv = document.createElement('div');
        floatDiv.classList.add('float-div');
        floatDiv.id = `item_${id}_floatdiv`;
        listingNameElement.parentElement.appendChild(floatDiv);

        let getFloatButton = createButton('Get Float', getFloatButtonClicked, 'green');
        floatDiv.appendChild(getFloatButton);

        // Create divs the following class names and append them to the button div
        let divClassNames = ['floatmessage', 'itemfloat', 'itemseed'];

        for (let className of divClassNames) {
            let div = document.createElement('div');
            div.classList.add(className);
            floatDiv.appendChild(div);
        }

        // check if we already have the float for this item
        if (id in floatData) {
            showFloat(id);
        }
    }

    // Add float utilities if it doesn't exist and we have valid items
    if (!document.querySelector('#floatUtilities') && listingRows.length > 0) {
        addFloatUtilities();
    }
};

// register the message listener in the page scope
let script = document.createElement('script');
script.innerText = `
    window.addEventListener('message', (e) => {
        if (e.data.type == 'requestListingInfo') {
            window.postMessage({
                type: 'listingInfo',
                listingInfo: g_rgListingInfo
            }, '*');
        }
    });
`;
document.head.appendChild(script);

floatTimer = setInterval(() => { addButtons(); }, 500);

// start the queue processing loop
processFloatQueue();

const logStyle = 'background: #222; color: #fff;';
console.log('%c CSGOFloat Market Checker (v1.2.0) by Step7750 ', logStyle);
console.log('%c Changelog can be found here: https://github.com/Step7750/CSGOFloat-Extension ', logStyle);
<commit_msg>Fallback to local storage if sync is not available<commit_after>let floatQueue = [];
let floatData = {};
let floatTimer;
let expressionTimer;
let steamListingInfo = {};
let listingInfoPromises = [];
let validExpressionVars = ['float', 'seed', 'minfloat', 'maxfloat'];
let filterStrings = [];
let filters = [];

// retrieve g_rgListingInfo from page script
window.addEventListener('message', (e) => {
    if (e.data.type == 'listingInfo') {
        steamListingInfo = e.data.listingInfo;

        // resolve listingInfoPromises
        for (let promise of listingInfoPromises) promise(steamListingInfo);

        listingInfoPromises = [];
    }
});

const retrieveListingInfoFromPage = function(listingId) {
    if (listingId != null && (listingId in steamListingInfo)) {
        return Promise.resolve(steamListingInfo);
    }

    window.postMessage({
        type: 'requestListingInfo'
    }, '*');

    return new Promise((resolve) => {
        listingInfoPromises.push(resolve);
    });
};

const getFloatData = function(listingId, inspectLink) {
    if (listingId in floatData) {
        return Promise.resolve({ iteminfo: floatData[listingId] });
    }

    return fetch(`https://api.csgofloat.com:1738/?url=${inspectLink}`)
    .then((response) => {
        if (response.ok) return response.json();
        return response.json().then((err) => { throw err; });
    });
};

const showFloat = function(listingId) {
    let itemInfo = floatData[listingId];

    let floatDiv = document.querySelector(`#item_${listingId}_floatdiv`);

    if (floatDiv) {
        // Remove the "get float" button
        let floatButton = floatDiv.querySelector('.float-btn');
        if (floatButton) floatDiv.removeChild(floatButton);

        // Remove message div
        let msgdiv = floatDiv.querySelector('.floatmessage');
        if (msgdiv) floatDiv.removeChild(msgdiv);

        // Add the float value
        let itemFloatDiv = floatDiv.querySelector('.itemfloat');
        if (itemFloatDiv) itemFloatDiv.innerText = `Float: ${itemInfo.floatvalue}`;

        // Add the paint seed
        let seedDiv = floatDiv.querySelector('.itemseed');
        if (seedDiv) seedDiv.innerText = `Paint Seed: ${itemInfo.paintseed}`;

        let vars = {
            'float': itemInfo.floatvalue,
            'seed': itemInfo.paintseed,
            'minfloat': itemInfo.min,
            'maxfloat': itemInfo.max
        };

        // Check to see if there is a filter match
        for (let filter of filters) {
            if (filter(vars) === 1) {
                // highlight the div
                floatDiv.parentNode.parentNode.style.backgroundColor = 'rgb(53, 73, 8)';
                break;
            }
        }
    }
};

const processFloatQueue = function() {
    if (floatQueue.length === 0) { return setTimeout(processFloatQueue, 100); }

    let lastItem = floatQueue.shift();

    let floatDiv = document.querySelector(`#item_${lastItem.listingId}_floatdiv`);

    if (!floatDiv) {
        // they have switched pages since initiating the request, so continue
        processFloatQueue();
        return;
    }

    let buttonText = floatDiv.querySelector('span');

    if (buttonText) buttonText.innerText = 'Fetching';

    getFloatData(lastItem.listingId, lastItem.inspectLink)
    .then((data) => {
        floatData[lastItem.listingId] = data.iteminfo;

        showFloat(lastItem.listingId);

        processFloatQueue();
    })
    .catch((err) => {
        // Reset the button text for this itemid
        if (buttonText) buttonText.innerText = 'Get Float';

        // Change the message div for this item to the error
        if (floatDiv) {
            floatDiv.querySelector('.floatmessage').innerText = err.error || 'Unknown Error';
        }

        processFloatQueue();
    });
};

// Puts all of the available items on the page into the queue for float retrieval
const getAllFloats = function() {
    retrieveListingInfoFromPage()
    .then((steamListingData) => {
        // Get all current items on the page (in proper order)
        let listingRows = document.querySelectorAll('.market_listing_row.market_recent_listing_row');

        for (let row of listingRows) {
            let id = row.id.replace('listing_', '');

            let listingData = steamListingData[id];

            let inspectLink = listingData.asset.market_actions[0].link
            .replace('%listingid%', id)
            .replace('%assetid%', listingData.asset.id);

            floatQueue.push({ listingId: id, inspectLink: inspectLink });
        }
    });
};

const addFilter = function () {
    let filter = document.querySelector('#float_expression_filter').value;

    try {
        let compiled = compileExpression(filter, {}, validExpressionVars);

        // We know it is a valid expression
        filterStrings.push(filter);
        filters.push(compiled);

        addFilterUI(filter);
        saveFilters();
    }
    catch (e) {
        return;
    }
};

const removeFilter = function(e) {
    let removeBtn = e.srcElement;

    // get the parent
    let thisFilterDiv = removeBtn.parentNode.parentNode;

    // Remove the button
    thisFilterDiv.removeChild(removeBtn.parentNode);

    // get the expression string
    let expression = thisFilterDiv.innerText;

    // remove the div
    document.querySelector('#floatFilters').removeChild(thisFilterDiv);

    // Remove it from the arrays
    let filterID = filterStrings.indexOf(expression.trim());

    if (filterID === -1) return;

    filterStrings.splice(filterID, 1);
    filters.splice(filterID, 1);

    saveFilters();
};

const filterKeyPress = function() {
    if (expressionTimer) clearTimeout(expressionTimer);

    expressionTimer = setTimeout(() => {
        let input = document.querySelector('#float_expression_filter');
        let compileError = document.querySelector('#compileError');
        let status = document.querySelector('#compileStatus');
        let addFilterBtn = document.querySelector('#addFloatFilter');

        let expression = input.value;

        // try to compile the expression
        try {
            compileExpression(expression, {}, validExpressionVars);
            status.setAttribute('error', 'false');
            status.innerText = '✓';
            compileError.innerText = '';
            addFilterBtn.style.display = '';
        }
        catch (e) {
            status.setAttribute('error', 'true');
            status.innerText = '✗';
            compileError.innerText = e.message;
            addFilterBtn.style.display = 'none';
        }
    }, 250);
};

const getSaveKey = function() {
    let itemname = document.querySelector('.market_listing_nav a:nth-child(2)');

    if (!itemname) return;
    else return itemname.innerText + '_expressions';
};

const getSavedFilters = function(cb) {
    let key = getSaveKey();

    if (!key) cb([]);

    let syncFilters = {};
    syncFilters[key] = [];

    let storageType = chrome.storage.sync;
    if (!storageType) storageType = chrome.storage.local;

    storageType.get(syncFilters, (items) => {
        cb(items[key]);
    });
};

const saveFilters = function() {
    let key = getSaveKey();

    if (!key) return;

    let syncFilters = {};
    syncFilters[key] = filterStrings;

    let storageType = chrome.storage.sync;
    if (!storageType) storageType = chrome.storage.local;

    storageType.set(syncFilters);
};

const addFilterUI = function(expression) {
    let parentDiv = document.querySelector('#floatFilters');

    let thisDiv = document.createElement('div');
    thisDiv.innerText = expression;

    // Add remove filter btn
    let removeFilterBtn = createButton('Remove Filter', removeFilter, 'grey');
    removeFilterBtn.style.marginTop = '-3px';
    removeFilterBtn.style.float = 'right';
    thisDiv.appendChild(removeFilterBtn);

    // Add line break
    let hr = document.createElement('hr');
    thisDiv.appendChild(hr);

    parentDiv.appendChild(thisDiv);
};

const createButton = function(text, eventListener, colour) {
    let btn = document.createElement('a');
    btn.classList.add(`btn_${colour}_white_innerfade`);
    btn.classList.add('btn_small');
    btn.classList.add('float-btn');
    btn.addEventListener('click', eventListener);

    let span = document.createElement('span');
    span.innerText = text;
    btn.appendChild(span);

    return btn;
};

const addFiltersDiv = function(parent) {
    let filterdiv = document.createElement('div');
    filterdiv.id = 'floatFilter';
    parent.appendChild(filterdiv);

    // Add separator
    let hr = document.createElement('hr');
    filterdiv.appendChild(hr);

    // Adds filters div
    let filtersdiv = document.createElement('div');
    filtersdiv.id = 'floatFilters';
    filterdiv.appendChild(filtersdiv);

    // Add new filter input box
    let input = document.createElement('input');
    input.id = 'float_expression_filter';
    input.classList.add('filter_search_box');
    input.placeholder = 'Add Float Highlight Filter';
    input.style.width = '350px';
    input.addEventListener('keyup', filterKeyPress);
    filterdiv.appendChild(input);

    // Add compile status indicator
    let status = document.createElement('div');
    status.id = 'compileStatus';
    filterdiv.appendChild(status);

    // Add new filter btn
    let addFilterBtn = createButton('Add Filter', addFilter, 'green');
    addFilterBtn.id = 'addFloatFilter';
    addFilterBtn.style.display = 'none';
    addFilterBtn.style.marginLeft = '10px';

    filterdiv.appendChild(addFilterBtn);

    // Compile error div
    let compileError = document.createElement('div');
    compileError.id = 'compileError';
    filterdiv.appendChild(compileError);

    // Add any saved filters
    getSavedFilters((expressions) => {
        for (let expression of expressions) {
            filterStrings.push(expression);
            filters.push(compileExpression(expression, {}, validExpressionVars));
            addFilterUI(expression);
        }
    });
};

// Adds float utilities
const addFloatUtilities = function() {
    let parentDiv = document.createElement('div');
    parentDiv.id = 'floatUtilities';

    // Add get all floats button
    let allFloatButton = createButton('Get All Floats', getAllFloats, 'green');
    parentDiv.appendChild(allFloatButton);

    // Add github link
    let githubLink = document.createElement('a');
    githubLink.classList.add('float-github');
    githubLink.href = 'https://github.com/Step7750/CSGOFloat';
    githubLink.innerText = 'Powered by CSGOFloat';
    parentDiv.appendChild(githubLink);

    // Add filter div
    addFiltersDiv(parentDiv);

    document.querySelector('#searchResultsTable').insertBefore(parentDiv, document.querySelector('#searchResultsRows'));
};

const getFloatButtonClicked = function(e) {
    let row = e.currentTarget.parentElement.parentElement.parentElement;
    let id = row.id.replace('listing_', '');

    retrieveListingInfoFromPage(id)
    .then((steamListingData) => {
        let listingData = steamListingData[id];

        if (!listingData) return;

        let inspectLink = listingData.asset.market_actions[0].link
        .replace('%listingid%', id)
        .replace('%assetid%', listingData.asset.id);

        floatQueue.push({ listingId: id, inspectLink: inspectLink });
    });
};

// If an item on the current page doesn't have the float div/buttons, this function adds it
const addButtons = function() {
    // Iterate through each item on the page
    let listingRows = document.querySelectorAll('.market_listing_row.market_recent_listing_row');

    for (let row of listingRows) {
        let id = row.id.replace('listing_', '');

        if (row.querySelector(`#item_${id}_floatdiv`)) { continue; }

        let listingNameElement = row.querySelector(`#listing_${id}_name`);

        let floatDiv = document.createElement('div');
        floatDiv.classList.add('float-div');
        floatDiv.id = `item_${id}_floatdiv`;
        listingNameElement.parentElement.appendChild(floatDiv);

        let getFloatButton = createButton('Get Float', getFloatButtonClicked, 'green');
        floatDiv.appendChild(getFloatButton);

        // Create divs the following class names and append them to the button div
        let divClassNames = ['floatmessage', 'itemfloat', 'itemseed'];

        for (let className of divClassNames) {
            let div = document.createElement('div');
            div.classList.add(className);
            floatDiv.appendChild(div);
        }

        // check if we already have the float for this item
        if (id in floatData) {
            showFloat(id);
        }
    }

    // Add float utilities if it doesn't exist and we have valid items
    if (!document.querySelector('#floatUtilities') && listingRows.length > 0) {
        addFloatUtilities();
    }
};

// register the message listener in the page scope
let script = document.createElement('script');
script.innerText = `
    window.addEventListener('message', (e) => {
        if (e.data.type == 'requestListingInfo') {
            window.postMessage({
                type: 'listingInfo',
                listingInfo: g_rgListingInfo
            }, '*');
        }
    });
`;
document.head.appendChild(script);

floatTimer = setInterval(() => { addButtons(); }, 500);

// start the queue processing loop
processFloatQueue();

const logStyle = 'background: #222; color: #fff;';
console.log('%c CSGOFloat Market Checker (v1.2.0) by Step7750 ', logStyle);
console.log('%c Changelog can be found here: https://github.com/Step7750/CSGOFloat-Extension ', logStyle);
<|endoftext|>


<commit_before>var Scaler = require('./Scaler');

var min = 1
var max = 5;

function ScenarioExecutor (restoreTime, scaler, timeout) {
    this.scaler = scaler || new Scaler();
    this.restoreTime = restoreTime;
    this.timeout = timeout || setTimeout;

    if (typeof this.restoreTime !== 'number') {
        this.restoreTime = Math.floor(Math.random() * (max - min + 1)) + min
    }
}

ScenarioExecutor.prototype.exec = function(composeFile, scenario, callback) {
    var containers = scenario.getContainers();

    var self = this;
    this.scaler.scale(composeFile, containers, function (err){
        if (err) {
            callback(err);
            return;
        }

        self.timeout(function() {
            self.scaler.restore(composeFile, containers, function() {
                callback();
            });
        }, self.restoreTime);
    });
};

module.exports = ScenarioExecutor;

<commit_msg>Add missing semi-colon<commit_after>var Scaler = require('./Scaler');

var min = 1
var max = 5;

function ScenarioExecutor (restoreTime, scaler, timeout) {
    this.scaler = scaler || new Scaler();
    this.restoreTime = restoreTime;
    this.timeout = timeout || setTimeout;

    if (typeof this.restoreTime !== 'number') {
        this.restoreTime = Math.floor(Math.random() * (max - min + 1)) + min;
    }
}

ScenarioExecutor.prototype.exec = function(composeFile, scenario, callback) {
    var containers = scenario.getContainers();

    var self = this;
    this.scaler.scale(composeFile, containers, function (err){
        if (err) {
            callback(err);
            return;
        }

        self.timeout(function() {
            self.scaler.restore(composeFile, containers, function() {
                callback();
            });
        }, self.restoreTime);
    });
};

module.exports = ScenarioExecutor;

<|endoftext|>