<commit_before>/**
 * The MIT License
 *
 * Copyright (c) 2016 Saravjeet 'Aman' Singh
 * <saravjeetamansingh@gmail.com>
 *
 * 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.
 *
 */

'use strict';

/* global Promise, define, module, exports, window */

// eslint-disable-next-line no-extra-semi
;(function(factory) {

    if ( typeof Promise === 'undefined' ) {
        throw new Error('scramblejs requires the Promise API');
    }

    if ( typeof module === 'object' &&
         typeof module.exports === 'object' ) {

        // this is a commonjs aware system
        // ... or someone has really gone and done it
        module.exports = factory();
    } else if ( typeof define === 'function' ) {

        // this is _probably_ an AMD system
        define([], factory);
    } else {

        // we are operating inside a browser environment
        // ... probably
        window.scramble = factory();
    }

})(function() {

    var exports = Object.create(null);
    var module = {
        exports: exports
    };

    var config = {
        minFlip: 1,
        maxFlip: 7,
        minInterval: 150,
        maxInterval: 100,
        minDelay: 10,
        maxDelay: 50
    };

    /**
     * __build_*__ act as a macro for substitution by the build
     * system
     */

    // eslint-disable-next-line no-undef
    var chars = __build_1__;

    var clone = function(obj) {
        return JSON.parse(JSON.stringify(obj));
    };

    var placeholder = function(ch) {
        return '' + ch + '';
    };

    var scaffoldElement = function(el) {

        if ( hasClass(el, 'js-scramble') ) {
            return;
        }

        var baseString = el.innerHTML.split('');
        var replacement = '';
        baseString.forEach(function(c) {
            replacement += placeholder(c);
        });

        el.innerHTML = replacement;

        var classes = el.className.split(' ');
        classes.push('js-scramble');
        el.className = classes.join(' ');
    };

    /**
     * randomChars(n)
     *
     * build a list of random characters
     */
    var randomChars = function(n) {
        var list = [];
        for ( var i = 0; i < n; ++i ) {
            var index = Math.floor(Math.random() * chars.length);
            list.push(chars[index]);
        }
        return list;
    };

    /**
     * randomValue(min, max)
     *
     * get a random value between min and mix
     */
    var randomValue = function(min, max) {
        return min + Math.floor(Math.random() * (max - min));
    };

    var hasClass = function(el, cls) {
        return el.className.indexOf(cls) !== -1;
    };

    /**
     * domListToArray(domList)
     *
     * convert DOM list to an Array. Doesn't work for for IE8 or less
     */
    var domListToArray = function(dl) {
        return Array.prototype.slice.call(dl, 0);
    };

    /**
     * queueAnimation(el, cb)
     *
     * This function is used to schedule animation for every element.
     *
     * cb is the function that is responsible for updating the
     * respective elements.  it takes the args (actor, ch, interval),
     * where actor is the dom element, ch is a list of characters, and
     * interval is the time between the character switches.
     */
    var queueAnimation = function(el, cb) {

        scaffoldElement(el);

        var actors = domListToArray(el.children);
        actors.forEach(function(actor) {
            var delay = randomValue(config.minDelay, config.maxDelay);
            setTimeout(function() {
                var n = randomValue(config.minFlip, config.maxFlip);
                var ch = randomChars(n);
                var interval = randomValue(config.minInterval, config.maxInterval);
                setTimeout(function() {
                    cb(actor, ch, interval);
                }, interval);
            }, delay);
        });
    };

    var setConfig = function(config, newConfig) {

        if ( typeof newConfig !== 'object' || newConfig.hasOwnProperty('length') ) {
            console.error('scramble: config: was expecting an object, got ' + newConfig);
            return;
        }

        var validProps = ['delay', 'flip', 'interval'];

        for ( var prop in newConfig ) {
            var p = prop[0].toUpperCase() + prop.slice(1);
            if ( validProps.indexOf(prop) === -1 ) {
                console.warn('scramble: config: unrecognized config parameter: ' + prop);
            } else if ( typeof newConfig[prop] === 'number' ) {
                config['min'+p] = newConfig[prop];
                config['max'+p] = newConfig[prop];
            } else if ( typeof newConfig[prop] === 'object' ) {
                config['min'+p] = newConfig[prop].min || config['min'+p];
                config['max'+p] = newConfig[prop].max || config['max'+p];
            } else {
                console.warn('scramble: config: config field values must be a number or an object');
            }
        }

        return config;
    };

    var Grinder = function(element) {

        if ( !(this instanceof Grinder) ) {
            throw Error('objects must be constructed using the new keyword');
        }

        this._origin = Promise.resolve(element);
        this._config = clone(config);
    };

    Grinder.prototype.enscramble = function() {

        var p = this._origin.then(function(el) {

            scaffoldElement(el);
            var count = el.children.length;
            return new Promise(function(resolve, reject) {

                function _enscramble(actor, chars, interval) {

                    if ( chars.length === 0 ) {
                        --count;
                        if ( count === 0 ) {
                            resolve(el);
                        }
                        return;
                    }

                    actor.innerHTML = chars.pop();
                    setTimeout(function() {
                        _enscramble(actor, chars, interval);
                    }, interval);
                }

                queueAnimation(el, _enscramble);
            });
        });

        return new Grinder(p);
    };

    Grinder.prototype.descramble = function() {

        var p = this._origin.then(function(el) {

            scaffoldElement(el);
            var count = el.children.length;
            return new Promise(function(resolve, reject) {

                function _descramble(actor, chars, interval) {

                    if ( chars.length === 0 ) {
                        actor.innerHTML = actor.dataset.ch;
                        --count;
                        if ( count === 0 ) {
                            resolve(el);
                        }
                        return;
                    }
                    actor.innerHTML = chars.pop();
                    setTimeout(function() {
                        _descramble(actor, chars, interval);
                    }, interval);
                }
                queueAnimation(el, _descramble);
            });
        });

        return new Grinder(p);
    };

    Grinder.prototype.then = function(cb) {

        var p = this._origin.then(cb);
        return new Grinder(p);
    };

    Grinder.prototype.catch = function(cb) {

        var p = this._origin.catch(cb);
        return new Grinder(p);
    };

    Grinder.prototype.wait = function(duration) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                setTimeout(function() {
                    resolve(el);
                }, duration);
            });
        });
        return new Grinder(p);
    };

    Grinder.prototype.setConfig = function(newConfig) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                setConfig(this._config, newConfig);
                resolve(el);
            });
        });

        return new Grinder(p);
    };

    Grinder.prototype.setText = function(text) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                if ( typeof text !== 'string' ) {
                    console.error('scramble: setText: text must be a string, got: ' + typeof text);
                    return reject('error');
                }

                scaffoldElement(el);

                var actors = el.children;
                for ( var i = 0; i < actors.length; ++i ) {
                    actors[i].dataset.ch = text[i] || ' ';
                }

                return resolve(el);
            });
        });

        return new Grinder(p);
    };

    Grinder.prototype.createEmpty = function(length) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                if ( typeof length !== 'number' ) {
                    console.error('scramble: createEmpty: length must be a number, got: ' + typeof length);
                    return;
                }

                var components = '';
                for ( var i = 0; i < length; ++ i ) {
                    components += placeholder(' ');
                }

                el.innerHTML = components;

                if ( !hasClass(el, 'js-scramble') ){
                    el.className += ' js-scramble';
                }

                return resolve(el);
            });
        });

        return new Grinder(p);
    };

    exports.select = function(sel) {

        var el;
        if ( sel instanceof Element ) {
            el = sel;
        } else {
            el = document.querySelector(sel);
        }

        return new Grinder(el);
    };

    exports.setConfigGlobal = function(newConfig) {

        setConfig(config, newConfig);
    };

    return module.exports;
});
<commit_msg>update setText<commit_after>/**
 * The MIT License
 *
 * Copyright (c) 2016 Saravjeet 'Aman' Singh
 * <saravjeetamansingh@gmail.com>
 *
 * 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.
 *
 */

'use strict';

/* global Promise, define, module, exports, window */

// eslint-disable-next-line no-extra-semi
;(function(factory) {

    if ( typeof Promise === 'undefined' ) {
        throw new Error('scramblejs requires the Promise API');
    }

    if ( typeof module === 'object' &&
         typeof module.exports === 'object' ) {

        // this is a commonjs aware system
        // ... or someone has really gone and done it
        module.exports = factory();
    } else if ( typeof define === 'function' ) {

        // this is _probably_ an AMD system
        define([], factory);
    } else {

        // we are operating inside a browser environment
        // ... probably
        window.scramble = factory();
    }

})(function() {

    var exports = Object.create(null);
    var module = { exports: exports };

    var config = {
        minFlip: 1,
        maxFlip: 7,
        minInterval: 150,
        maxInterval: 100,
        minDelay: 10,
        maxDelay: 50
    };

    /**
     * __build_*__ act as a macro for substitution by the build
     * system
     */

    // eslint-disable-next-line no-undef
    var chars = __build_1__;

    var clone = function(obj) {
        return JSON.parse(JSON.stringify(obj));
    };

    var placeholder = function(ch) {
        return '' + ch + '';
    };

    var scaffoldElement = function(el) {

        if ( !(el instanceof Element) ) {
            var message = "scramble: not a DOM Element";
            console.warn(message);
            throw new Error(message);
        }

        if ( hasClass(el, 'js-scramble') ) {
            return;
        }

        var baseString = el.innerHTML.split('');
        var replacement = '';
        baseString.forEach(function(c) {
            replacement += placeholder(c);
        });

        el.innerHTML = replacement;

        var classes = el.className.split(' ');
        classes.push('js-scramble');
        el.className = classes.join(' ');
    };

    /**
     * randomChars(n)
     *
     * build a list of random characters
     */
    var randomChars = function(n) {
        var list = [];
        for ( var i = 0; i < n; ++i ) {
            var index = Math.floor(Math.random() * chars.length);
            list.push(chars[index]);
        }
        return list;
    };

    /**
     * randomValue(min, max)
     *
     * get a random value between min and mix
     */
    var randomValue = function(min, max) {
        return min + Math.floor(Math.random() * (max - min));
    };

    var hasClass = function(el, cls) {
        return el.className.indexOf(cls) !== -1;
    };

    /**
     * domListToArray(domList)
     *
     * convert DOM list to an Array. Doesn't work for for IE8 or less
     */
    var domListToArray = function(dl) {
        return Array.prototype.slice.call(dl, 0);
    };

    /**
     * queueAnimation(el, cb)
     *
     * This function is used to schedule animation for every element.
     *
     * cb is the function that is responsible for updating the
     * respective elements.  it takes the args (actor, ch, interval),
     * where actor is the dom element, ch is a list of characters, and
     * interval is the time between the character switches.
     */
    var queueAnimation = function(el, cb) {

        scaffoldElement(el);

        var actors = domListToArray(el.children);
        actors.forEach(function(actor) {
            var delay = randomValue(config.minDelay, config.maxDelay);
            setTimeout(function() {
                var n = randomValue(config.minFlip, config.maxFlip);
                var ch = randomChars(n);
                var interval = randomValue(config.minInterval, config.maxInterval);
                setTimeout(function() {
                    cb(actor, ch, interval);
                }, interval);
            }, delay);
        });
    };

    var setConfig = function(config, newConfig) {

        if ( typeof newConfig !== 'object' || newConfig.hasOwnProperty('length') ) {
            console.error('scramble: config: was expecting an object, got ' + newConfig);
            return;
        }

        var validProps = ['delay', 'flip', 'interval'];

        for ( var prop in newConfig ) {
            var p = prop[0].toUpperCase() + prop.slice(1);
            if ( validProps.indexOf(prop) === -1 ) {
                console.warn('scramble: config: unrecognized config parameter: ' + prop);
            } else if ( typeof newConfig[prop] === 'number' ) {
                config['min'+p] = newConfig[prop];
                config['max'+p] = newConfig[prop];
            } else if ( typeof newConfig[prop] === 'object' ) {
                config['min'+p] = newConfig[prop].min || config['min'+p];
                config['max'+p] = newConfig[prop].max || config['max'+p];
            } else {
                console.warn('scramble: config: config field values must be a number or an object');
            }
        }

        return config;
    };

    var Grinder = function(element) {

        if ( !(this instanceof Grinder) ) {
            throw Error('objects must be constructed using the new keyword');
        }

        this._origin = Promise.resolve(element);
        this._config = clone(config);
    };

    Grinder.prototype.enscramble = function() {

        var p = this._origin.then(function(el) {

            scaffoldElement(el);
            var count = el.children.length;
            return new Promise(function(resolve, reject) {

                function _enscramble(actor, chars, interval) {

                    if ( chars.length === 0 ) {
                        --count;
                        if ( count === 0 ) {
                            resolve(el);
                        }
                        return;
                    }

                    actor.innerHTML = chars.pop();
                    setTimeout(function() {
                        _enscramble(actor, chars, interval);
                    }, interval);
                }

                queueAnimation(el, _enscramble);
            });
        });

        return new Grinder(p);
    };

    Grinder.prototype.descramble = function() {

        var p = this._origin.then(function(el) {

            scaffoldElement(el);
            var count = el.children.length;
            return new Promise(function(resolve, reject) {

                function _descramble(actor, chars, interval) {

                    if ( chars.length === 0 ) {
                        actor.innerHTML = actor.dataset.ch;
                        --count;
                        if ( count === 0 ) {
                            resolve(el);
                        }
                        return;
                    }
                    actor.innerHTML = chars.pop();
                    setTimeout(function() {
                        _descramble(actor, chars, interval);
                    }, interval);
                }
                queueAnimation(el, _descramble);
            });
        });

        return new Grinder(p);
    };

    Grinder.prototype.then = function(cb) {

        var p = this._origin.then(cb);
        return new Grinder(p);
    };

    Grinder.prototype.catch = function(cb) {

        var p = this._origin.catch(cb);
        return new Grinder(p);
    };

    Grinder.prototype.wait = function(duration) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                setTimeout(function() {
                    resolve(el);
                }, duration);
            });
        });
        return new Grinder(p);
    };

    Grinder.prototype.setConfig = function(newConfig) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                setConfig(this._config, newConfig);
                resolve(el);
            });
        });

        return new Grinder(p);
    };

    var textAlignment = exports.align = {
        left: function(actors, text) {
            for ( var i = 0; i < actors.length; ++i ) {
                actors[i].dataset.ch = text[i] || '&nbsp';
            }
        },
        right: function(actors, text) {
            var start = actors.length - text.length;
            for ( var i = 0; i < actors.length; ++i ) {
                var ch;
                if ( i < start ) {
                    ch = '&nbsp';
                } else {
                    ch = text[i-start];
                }
                actors[i].dataset.ch = ch;
            }
        },
        center: function(actors, text) {
            var padding = actors.length - text.length;
            var start = padding / 2;
            var end = actors.length - padding + start;
            for ( var i = 0; i < actors.length; ++i ) {
                var ch;
                if ( i < start || i >= end ) {
                    ch = '&nbsp';
                } else {
                    ch = text[i-start];
                }
                actors[i].dataset.ch = ch;
            }
        }
    };

    Grinder.prototype.setText = function(text, align) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                if ( typeof text !== 'string' ) {
                    console.error('scramble: setText: text must be a string, got: ' + typeof text);
                    return reject('error');
                }

                var i;
                scaffoldElement(el);
                var actors = el.children;

                if ( text.length > actors.length ) {

                    var padding = text.length - actors.length;
                    for ( i = 0; i < padding; ++i ) {
                        el.appendChild(document.createElement('span'));
                    }
                }

                align = align || textAlignment.left;
                if ( typeof align !== 'function' ) {
                    console.warn('scramble: setText: \'align\' must be a function (fallback to left alignment)');
                    align = textAlignment.left;
                }
                align(actors, text);

                return resolve(el);
            });
        });

        return new Grinder(p);
    };

    Grinder.prototype.createEmpty = function(length) {

        var p = this._origin.then(function(el) {

            return new Promise(function(resolve, reject) {

                if ( typeof length !== 'number' ) {
                    console.error('scramble: createEmpty: length must be a number, got: ' + typeof length);
                    return;
                }

                var components = '';
                for ( var i = 0; i < length; ++ i ) {
                    components += placeholder('');
                }

                el.innerHTML = components;

                if ( !hasClass(el, 'js-scramble') ){
                    el.className += ' js-scramble';
                }

                return resolve(el);
            });
        });

        return new Grinder(p);
    };

    exports.select = function(sel) {

        var el;
        if ( sel instanceof Element ) {
            el = sel;
        } else {
            el = document.querySelector(sel);
        }

        return new Grinder(el);
    };

    exports.setConfigGlobal = function(newConfig) {

        setConfig(config, newConfig);
    };

    return module.exports;
});
<|endoftext|>


<commit_before>var Registry = require("./core/registry");
var Device = require('./core/device');
var Layer = require('./core/layer');
var PaperView = require("./view/paperView");
var ViewManager = require("./view/viewManager");
var AdaptiveGrid = require("./view/grid/adaptiveGrid");
var PageSetup = require("./view/pageSetup");
var Colors = require("./view/colors");
var ThreeDeviceRenderer = require("./view/render3D/ThreeDeviceRenderer");
var Examples = require("./examples/jsonExamples");

var view;
var viewManager;
var grid;

paper.setup("c");

window.onload = function() {
    view = new PaperView(document.getElementById("c"));
    viewManager = new ViewManager(view);
    grid = new AdaptiveGrid();
    grid.setColor(Colors.BLUE_500);

    Registry.viewManager = viewManager;

    viewManager.loadDeviceFromJSON(JSON.parse(Examples.example1));
    viewManager.updateGrid();
    Registry.currentDevice.updateView();

    window.dev = Registry.currentDevice;
    window.Registry = Registry;

    window.view = Registry.viewManager.view;

    Registry.threeRenderer = new ThreeDeviceRenderer(document.getElementById("renderContainer"));
    PageSetup.setupAppPage();

};<commit_msg>Added the ability read the query params to load json on load<commit_after>var Registry = require("./core/registry");
var Device = require('./core/device');
var Layer = require('./core/layer');
var PaperView = require("./view/paperView");
var ViewManager = require("./view/viewManager");
var AdaptiveGrid = require("./view/grid/adaptiveGrid");
var PageSetup = require("./view/pageSetup");
var Colors = require("./view/colors");
var ThreeDeviceRenderer = require("./view/render3D/ThreeDeviceRenderer");
var Examples = require("./examples/jsonExamples");

var view;
var viewManager;
var grid;

paper.setup("c");

function getQueryVariable(variable)
{
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i=0;i<vars.length;i++) {
        var pair = vars[i].split("=");
        if(pair[0] == variable){return pair[1];}
    }
    return(false);
}

window.onload = function() {
    view = new PaperView(document.getElementById("c"));
    viewManager = new ViewManager(view);
    grid = new AdaptiveGrid();
    grid.setColor(Colors.BLUE_500);

    Registry.viewManager = viewManager;

    viewManager.loadDeviceFromJSON(JSON.parse(Examples.example1));
    viewManager.updateGrid();
    Registry.currentDevice.updateView();

    window.dev = Registry.currentDevice;
    window.Registry = Registry;

    window.view = Registry.viewManager.view;

    Registry.threeRenderer = new ThreeDeviceRenderer(document.getElementById("renderContainer"));
    PageSetup.setupAppPage();

    if(false != getQueryVariable("file")){
        //Download the json
        var url = decodeURIComponent(getQueryVariable("file"));
        fetch(url) // Call the fetch function passing the url of the API as a parameter
            .then((resp) => resp.json())
            .then(function(data) {
                // Create and append the li's to the ul
                alert(data);
                console.log(data);
                viewManager.loadDeviceFromJSON(data);
                viewManager.updateGrid();
                Registry.currentDevice.updateView();

                window.dev = Registry.currentDevice;
                window.Registry = Registry;

                window.view = Registry.viewManager.view;

                Registry.threeRenderer = new ThreeDeviceRenderer(document.getElementById("renderContainer"));
                PageSetup.setupAppPage();

            })
            .catch(function(err) {
                // This is where you run code if the server returns any errors
                alert("Error fetching the json");
                alert(err)
            });
    }

};

<|endoftext|>


<commit_before>import {isFunction, isObject, isUndefined} from '../../shared/util'

function param(obj){
    const items = []
    Object.keys(obj).forEach((key)=>{
        let val = obj[key]
        if(val === null || isUndefined(val)) return
        items.push(encodeURIComponent(key) + '=' + encodeURIComponent(val))
    })
    return items.join('&')
}

/**
 * overloads
 * - http(url) //http get request
 * - http(url, done) //http get request
 * - http(url, method) //http request with specified method
 * - http(url, method, done) //http request with specified method
 * - http(url, method, data) //http request with specified method and data
 * - http(url, method, data, done) //http request with specified method and data
 * - http({url, method, data, headers, done}) //
 */
export function http(url, method, data, done) {
    let req
    if(isObject(url)){
        req = url
    }else{
        req = {}
        req.url = url
        if (isFunction(method)) {
            req.done = method
        } else if(isFunction(data)) {
            req.done = data
            req.method = method
        }else if(isFunction(done)){
            req.done = done
            req.method = method
            req.data = data
        }
    }
    if(!req.url) return
    req.method = req.method || 'GET'
    if(req.method === 'GET' && req.data){
        if(/\?/.test(req.url)){
            req.url += '&' + param(req.data)
        }else{
            req.url += '?' + param(req.data)
        }
    }

    let isCompleted = false
    function complete(err, data){
        if(isCompleted){
            return
        }
        isCompleted = true

        try{
            if(isFunction(req.done)){
                req.done.call(xhr,err,data)
            }
        }finally{
            if(isFunction(http.oncomplete)){
                http.oncomplete.call(this)
            }
        }
    }

    var xhr = new XMLHttpRequest()
    xhr.timeout = req.timeout || 120 * 1000
    xhr.ontimeout = ()=> complete(new Error('request timeout'))
    xhr.open(req.method, req.url, true)
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            complete(null)
        }
    }

    if(req.headers){
        Object.keys(req.headers).forEach(key =>{
            xhr.setRequestHeader(key, req.headers[key])
        })
    }

    if(isFunction(http.onstart)){
        http.onstart.call(this)
    }

    xhr.send(req.method === 'GET' ? null : req.data)
    return ()=>{
        xhr.abort()
        complete(new Error('request aborted'))
    }
}

http.get = function(url, data, done){
    return http(url, 'GET', data, done)
}

http.post = function(url, data, done){
    return http(url, 'POST', data, done)
}

http.put = function(url, data, done){
    return http(url, 'PUT', data, done)
}

http.delete = function(url, data, done){
    return http(url, 'DELETE', data, done)
}

//http.onstart = function(){}
//http.oncomplete = function(){}
export default http

export function resource(baseUrl = ''){
    const rootUrl = baseUrl[baseUrl.length-1] === '/'
        ? baseUrl
        : baseUrl + '/'

    function parseJSON(xhr, err, done){
        if(err) return done(err)
        if(xhr.status !== 200) {
            return done(new Error('http error: ' + xhr.status + '\n' + xhr.statusText))
        }
        try{
            let text = xhr.responseText
            if(text){
                done(null,JSON.parse(text))
            }else{
                done(null, null)
            }
        }catch(error){
            done(error)
        }
    }
    return {
        items(filter, done){
            return http.get(rootUrl, filter, function(err){
                parseJSON(this, err, done)
            })
        },
        get(id, done){
            let url = rootUrl  + id
            return http.get(url, function(err){
                parseJSON(this, err, done)
            })
        },
        add(data, done){
            return http.put(rootUrl, data, function(err){
                parseJSON(this, err, done)
            })
        },
        update(id, data, done){
            let url = rootUrl  + id
            return http.post(url, data, function(err){
                parseJSON(this, err, done)
            })
        },
        remove(id, done){
            let url = rootUrl  + id
            return http.delete(url, function (err) {
                if(err) return done(err)
                if(this.status !== 200) {
                    return done(new Error('http error: ' + this.status + '\n' + this.statusText))
                }
            })
        }
    }
}<commit_msg>make resource work with non-restful request<commit_after>import {isFunction, isObject, isUndefined} from '../../shared/util'

function param(obj){
    const items = []
    Object.keys(obj).forEach((key)=>{
        let val = obj[key]
        if(val === null || isUndefined(val)) return
        items.push(encodeURIComponent(key) + '=' + encodeURIComponent(val))
    })
    return items.join('&')
}

/**
 * overloads
 * - http(url) //http get request
 * - http(url, done) //http get request
 * - http(url, method) //http request with specified method
 * - http(url, method, done) //http request with specified method
 * - http(url, method, data) //http request with specified method and data
 * - http(url, method, data, done) //http request with specified method and data
 * - http({url, method, data, headers, timeout, done}) //
 */
export function http(url, method, data, done) {
    let req
    if(isObject(url)){
        req = url
    }else{
        req = {}
        req.url = url
        if (isFunction(method)) {
            req.done = method
        } else if(isFunction(data)) {
            req.done = data
            req.method = method
        }else if(isFunction(done)){
            req.done = done
            req.method = method
            req.data = data
        }
    }
    if(!req.url) return
    req.method = (req.method || 'GET').toUpperCase()
    if(req.method === 'GET' && req.data){
        if(/\?/.test(req.url)){
            req.url += '&' + param(req.data)
        }else{
            req.url += '?' + param(req.data)
        }
    }

    let isCompleted = false
    function complete(err, data){
        if(isCompleted){
            return
        }
        isCompleted = true

        try{
            if(isFunction(req.done)){
                req.done.call(xhr,err,data)
            }
        }finally{
            if(isFunction(http.oncomplete)){
                http.oncomplete.call(this)
            }
        }
    }

    var xhr = new XMLHttpRequest()
    xhr.timeout = req.timeout || 120 * 1000
    xhr.ontimeout = ()=> complete(new Error('request timeout'))
    xhr.open(req.method, req.url, true)
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            complete(null)
        }
    }

    if(req.headers){
        Object.keys(req.headers).forEach(key =>{
            xhr.setRequestHeader(key, req.headers[key])
        })
    }

    if(isFunction(http.onstart)){
        http.onstart.call(this)
    }

    xhr.send(req.method === 'GET' ? null : req.data)

    let isCancelled = false
    return {
        cancel(){
            if(isCompleted || isCancelled) return
            isCancelled = true
            xhr.abort()
            complete(new Error('request aborted'))
        },
        get completed(){
            return isCompleted
        },
        get cancelled(){
            return isCancelled
        }
    }
}

http.get = function(url, data, done){
    return http(url, 'GET', data, done)
}

http.post = function(url, data, done){
    return http(url, 'POST', data, done)
}

http.put = function(url, data, done){
    return http(url, 'PUT', data, done)
}

http.delete = function(url, data, done){
    return http(url, 'DELETE', data, done)
}

//http.onstart = function(){}
//http.oncomplete = function(){}
export default http

/**
 * @param {string} baseUrl
 * @param {(baseUrl:string, id:any, action:string) => {url:string, method?:='GET',data?:any, before?: (done,err,data)=>void}} buildRequest
 * @export
 */
export function resource(baseUrl = './', buildRequest){
    const rootUrl = baseUrl[baseUrl.length-1] === '/'
        ? baseUrl
        : baseUrl + '/'
    const fn = buildRequest || ((rootUrl, id, action) => {
        let before = null
        if(action === 'remove'){
            before = function(done, err, data){
                if(err) return done(err)
                if(this.status !== 200) {
                    return done(new Error('http error: ' + this.status + '\n' + this.statusText))
                }
                done(null, data)
            }
        }
        let url = rootUrl
        if(id) url += id
        return {url, before}
    })

    function parseJSON(xhr, err, done){
        if(err) return done(err)
        if(xhr.status !== 200) {
            return done(new Error('http error: ' + xhr.status + '\n' + xhr.statusText))
        }
        try{
            let text = xhr.responseText
            if(text){
                done(null,JSON.parse(text))
            }else{
                done(null, null)
            }
        }catch(error){
            done(error)
        }
    }

    function makeRequest({action, done, data, id, method = 'GET'}){
        let req = fn(rootUrl, id, action)
        req.method = req.method || method
        req.data = data
        delete req.done
        if(req.before){
            req.done = req.before.bind(done)
        }else{
            req.done = function (err) {
                parseJSON(this, err, done)
            }
        }
        return http(req)
    }

    return {
        items(filter, done){
            return makeRequest({action: 'items', done, data: filter})
        },
        get(id, done){
            return makeRequest({action: 'get', done, id})
        },
        add(data, done){
            return makeRequest({action: 'add', done, method: 'PUT'})
        },
        update(id, data, done){
            return makeRequest({action: 'update', done, data, id, method: 'POST'})
        },
        remove(id, done){
            return makeRequest({action: 'remove', done, id, method: 'DELETE'})
        }
    }
}<|endoftext|>


<commit_before>/*
 * This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items on
 * the page have a hint marker displayed containing a sequence of letters. Typing those letters will select
 * a link.
 *
 * The characters we use to show link hints are a user-configurable option. By default they're the home row.
 * The CSS which is used on the link hints is also a configurable option.
 */

var hintMarkers = [];
var hintMarkerContainingDiv = null;
// The characters that were typed in while in "link hints" mode.
var hintKeystrokeQueue = [];
var linkHintsModeActivated = false;
var shouldOpenLinkHintInNewTab = false;
var shouldOpenLinkHintWithQueue = false;
// Whether link hint's "open in current/new tab" setting is currently toggled
var openLinkModeToggle = false;
// Whether we have added to the page the CSS needed to display link hints.
var linkHintsCssAdded = false;

/*
 * Generate an XPath describing what a clickable element is.
 * The final expression will be something like "//button | //xhtml:button | ..."
 */
var clickableElementsXPath = (function() {
  var clickableElements = ["a", "textarea", "button", "select", "input[not(@type='hidden')]",
                           "*[@onclick or @tabindex or @role='link' or @role='button']"];
  var xpath = [];
  for (var i in clickableElements)
    xpath.push("//" + clickableElements[i], "//xhtml:" + clickableElements[i]);
  return xpath.join(" | ")
})();

// We need this as a top-level function because our command system doesn't yet support arguments.
function activateLinkHintsModeToOpenInNewTab() { activateLinkHintsMode(true, false); }

function activateLinkHintsModeWithQueue() { activateLinkHintsMode(true, true); }

function activateLinkHintsMode(openInNewTab, withQueue) {
  if (!linkHintsCssAdded)
    addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js
  linkHintCssAdded = true;
  linkHintsModeActivated = true;
  setOpenLinkMode(openInNewTab, withQueue);
  buildLinkHints();
  document.addEventListener("keydown", onKeyDownInLinkHintsMode, true);
  document.addEventListener("keyup", onKeyUpInLinkHintsMode, true);
}

function setOpenLinkMode(openInNewTab, withQueue) {
  shouldOpenLinkHintInNewTab = openInNewTab;
  shouldOpenLinkHintWithQueue = withQueue;
  if (shouldOpenLinkHintWithQueue) {
    HUD.show("Open multiple links in a new tab");
  } else {
    if (shouldOpenLinkHintInNewTab)
      HUD.show("Open link in new tab");
    else
      HUD.show("Open link in current tab");
  }
}

/*
 * Builds and displays link hints for every visible clickable item on the page.
 */
function buildLinkHints() {
  var visibleElements = getVisibleClickableElements();

  // Initialize the number used to generate the character hints to be as many digits as we need to
  // highlight all the links on the page; we don't want some link hints to have more chars than others.
  var digitsNeeded = Math.ceil(logXOfBase(visibleElements.length, settings.linkHintCharacters.length));
  var linkHintNumber = 0;
  for (var i = 0; i < visibleElements.length; i++) {
    hintMarkers.push(createMarkerFor(visibleElements[i], linkHintNumber, digitsNeeded));
    linkHintNumber++;
  }
  // Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
  // because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
  // that if you scroll the page and the link has position=fixed, the marker will not stay fixed.
  // Also note that adding these nodes to document.body all at once is significantly faster than one-by-one.
  hintMarkerContainingDiv = document.createElement("div");
  hintMarkerContainingDiv.className = "internalVimiumHintMarker";
  for (var i = 0; i < hintMarkers.length; i++)
    hintMarkerContainingDiv.appendChild(hintMarkers[i]);
  document.body.appendChild(hintMarkerContainingDiv);
}

function logXOfBase(x, base) { return Math.log(x) / Math.log(base); }

/*
 * Returns all clickable elements that are not hidden and are in the current viewport.
 * We prune invisible elements partly for performance reasons, but moreso it's to decrease the number
 * of digits needed to enumerate all of the links on screen.
 */
function getVisibleClickableElements() {
  var resultSet = document.evaluate(clickableElementsXPath, document.body,
    function (namespace) {
      return namespace == "xhtml" ? "http://www.w3.org/1999/xhtml" : null;
    },
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  var visibleElements = [];

  // Find all visible clickable elements.
  for (var i = 0; i < resultSet.snapshotLength; i++) {
    var element = resultSet.snapshotItem(i);
    var clientRect = element.getClientRects()[0];

    if (isVisible(element, clientRect))
      visibleElements.push({element: element, rect: clientRect});

    // If the link has zero dimensions, it may be wrapping visible
    // but floated elements. Check for this.
    if (clientRect && (clientRect.width == 0 || clientRect.height == 0)) {
      for (var j = 0; j < element.children.length; j++) {
        if (window.getComputedStyle(element.children[j], null).getPropertyValue('float') != 'none') {
          var childClientRect = element.children[j].getClientRects()[0];
          if (isVisible(element.children[j], childClientRect)) {
            visibleElements.push({element: element.children[j], rect: childClientRect});
            break;
          }
        }
      }
    }
  }
  return visibleElements;
}

/*
 * Returns true if element is visible.
 */
function isVisible(element, clientRect) {
  // Exclude links which have just a few pixels on screen, because the link hints won't show for them anyway.
  var zoomFactor = currentZoomLevel / 100.0;
  if (!clientRect || clientRect.top < 0 || clientRect.top * zoomFactor >= window.innerHeight - 4 ||
      clientRect.left < 0 || clientRect.left * zoomFactor >= window.innerWidth - 4)
    return false;

  if (clientRect.width < 3 || clientRect.height < 3)
    return false;

  // eliminate invisible elements (see test_harnesses/visibility_test.html)
  var computedStyle = window.getComputedStyle(element, null);
  if (computedStyle.getPropertyValue('visibility') != 'visible' ||
      computedStyle.getPropertyValue('display') == 'none')
    return false;

  return true;
}

function onKeyDownInLinkHintsMode(event) {
  console.log("Key Down");
  if (event.keyCode == keyCodes.shiftKey && !openLinkModeToggle) {
    // Toggle whether to open link in a new or current tab.
    setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
    openLinkModeToggle = true;
  }

  var keyChar = getKeyChar(event);
  if (!keyChar)
    return;

  // TODO(philc): Ignore keys that have modifiers.
  if (isEscape(event)) {
    deactivateLinkHintsMode();
  } else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
    if (hintKeystrokeQueue.length == 0) {
      deactivateLinkHintsMode();
    } else {
      hintKeystrokeQueue.pop();
      updateLinkHints();
    }
  } else if (settings.linkHintCharacters.indexOf(keyChar) >= 0) {
    hintKeystrokeQueue.push(keyChar);
    updateLinkHints();
  } else {
    return;
  }

  event.stopPropagation();
  event.preventDefault();
}

function onKeyUpInLinkHintsMode(event) {
  if (event.keyCode == keyCodes.shiftKey && openLinkModeToggle) {
    // Revert toggle on whether to open link in new or current tab.
    setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
    openLinkModeToggle = false;
  }
  event.stopPropagation();
  event.preventDefault();
}

/*
 * Updates the visibility of link hints on screen based on the keystrokes typed thus far. If only one
 * link hint remains, click on that link and exit link hints mode.
 */
function updateLinkHints() {
  var matchString = hintKeystrokeQueue.join("");
  var linksMatched = highlightLinkMatches(matchString);
  if (linksMatched.length == 0)
    deactivateLinkHintsMode();
  else if (linksMatched.length == 1) {
    var matchedLink = linksMatched[0];
    if (isSelectable(matchedLink)) {
      matchedLink.focus();
      // When focusing a textbox, put the selection caret at the end of the textbox's contents.
      matchedLink.setSelectionRange(matchedLink.value.length, matchedLink.value.length);
      deactivateLinkHintsMode();
    } else {
      // When we're opening the link in the current tab, don't navigate to the selected link immediately;
      // we want to give the user some feedback depicting which link they've selected by focusing it.
      if (shouldOpenLinkHintWithQueue) {
        simulateClick(matchedLink);
        resetLinkHintsMode();
      } else if (shouldOpenLinkHintInNewTab) {
        simulateClick(matchedLink);
        matchedLink.focus();
        deactivateLinkHintsMode();
      } else {
        setTimeout(function() { simulateClick(matchedLink); }, 400);
        matchedLink.focus();
        deactivateLinkHintsMode();
      }
    }
  }
}

/*
 * Selectable means the element has a text caret; this is not the same as "focusable".
 */
function isSelectable(element) {
  var selectableTypes = ["search", "text", "password"];
  return (element.tagName == "INPUT" && selectableTypes.indexOf(element.type) >= 0) ||
      element.tagName == "TEXTAREA";
}

/*
 * Hides link hints which do not match the given search string. To allow the backspace key to work, this
 * will also show link hints which do match but were previously hidden.
 */
function highlightLinkMatches(searchString) {
  var linksMatched = [];
  for (var i = 0; i < hintMarkers.length; i++) {
    var linkMarker = hintMarkers[i];
    if (linkMarker.getAttribute("hintString").indexOf(searchString) == 0) {
      if (linkMarker.style.display == "none")
        linkMarker.style.display = "";
      for (var j = 0; j < linkMarker.childNodes.length; j++)
        linkMarker.childNodes[j].className = (j >= searchString.length) ? "" : "matchingCharacter";
      linksMatched.push(linkMarker.clickableItem);
    } else {
      linkMarker.style.display = "none";
    }
  }
  return linksMatched;
}

/*
 * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of
 * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits.
 */
function numberToHintString(number, numHintDigits) {
  var base = settings.linkHintCharacters.length;
  var hintString = [];
  var remainder = 0;
  do {
    remainder = number % base;
    hintString.unshift(settings.linkHintCharacters[remainder]);
    number -= remainder;
    number /= Math.floor(base);
  } while (number > 0);

  // Pad the hint string we're returning so that it matches numHintDigits.
  var hintStringLength = hintString.length;
  for (var i = 0; i < numHintDigits - hintStringLength; i++)
    hintString.unshift(settings.linkHintCharacters[0]);
  return hintString.join("");
}

function simulateClick(link) {
  var event = document.createEvent("MouseEvents");
  // When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on windows)
  // to open it in a new tab if necessary.
  var metaKey = (platform == "Mac" && shouldOpenLinkHintInNewTab);
  var ctrlKey = (platform != "Mac" && shouldOpenLinkHintInNewTab);
  event.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null);

  // Debugging note: Firefox will not execute the link's default action if we dispatch this click event,
  // but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
  link.dispatchEvent(event);
}

function deactivateLinkHintsMode() {
  if (hintMarkerContainingDiv)
    hintMarkerContainingDiv.parentNode.removeChild(hintMarkerContainingDiv);
  hintMarkerContainingDiv = null;
  hintMarkers = [];
  hintKeystrokeQueue = [];
  document.removeEventListener("keydown", onKeyDownInLinkHintsMode, true);
  document.removeEventListener("keyup", onKeyUpInLinkHintsMode, true);
  linkHintsModeActivated = false;
  HUD.hide();
}

function resetLinkHintsMode() {
  deactivateLinkHintsMode();
  activateLinkHintsModeWithQueue();
}

/*
 * Creates a link marker for the given link.
 */
function createMarkerFor(link, linkHintNumber, linkHintDigits) {
  var hintString = numberToHintString(linkHintNumber, linkHintDigits);
  var marker = document.createElement("div");
  marker.className = "internalVimiumHintMarker vimiumHintMarker";
  var innerHTML = [];
  // Make each hint character a span, so that we can highlight the typed characters as you type them.
  for (var i = 0; i < hintString.length; i++)
    innerHTML.push("<span>" + hintString[i].toUpperCase() + "</span>");
  marker.innerHTML = innerHTML.join("");
  marker.setAttribute("hintString", hintString);

  // Note: this call will be expensive if we modify the DOM in between calls.
  var clientRect = link.rect;
  var offsets = {};
  if (/^(absolute|fixed|relative)$/.test(window.getComputedStyle(document.body).position)) {
    var bodyRect = document.body.getClientRects()[0];
    offsets.x = -bodyRect.left;
    offsets.y = -bodyRect.top;
  } else {
    // The coordinates given by the window do not have the zoom factor included since the zoom is set only on
    // the document node.
    var zoomFactor = currentZoomLevel / 100.0;
    offsets.x = window.scrollX / zoomFactor;
    offsets.y = window.scrollY / zoomFactor;
  }
  marker.style.left = clientRect.left + offsets.x + "px";
  marker.style.top = clientRect.top  + offsets.y + "px";

  marker.clickableItem = link.element;
  return marker;
}
<commit_msg>Revert "Fix hint marker positioning when body's position is not static. Closes #140."<commit_after>/*
 * This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items on
 * the page have a hint marker displayed containing a sequence of letters. Typing those letters will select
 * a link.
 *
 * The characters we use to show link hints are a user-configurable option. By default they're the home row.
 * The CSS which is used on the link hints is also a configurable option.
 */

var hintMarkers = [];
var hintMarkerContainingDiv = null;
// The characters that were typed in while in "link hints" mode.
var hintKeystrokeQueue = [];
var linkHintsModeActivated = false;
var shouldOpenLinkHintInNewTab = false;
var shouldOpenLinkHintWithQueue = false;
// Whether link hint's "open in current/new tab" setting is currently toggled
var openLinkModeToggle = false;
// Whether we have added to the page the CSS needed to display link hints.
var linkHintsCssAdded = false;

/*
 * Generate an XPath describing what a clickable element is.
 * The final expression will be something like "//button | //xhtml:button | ..."
 */
var clickableElementsXPath = (function() {
  var clickableElements = ["a", "textarea", "button", "select", "input[not(@type='hidden')]",
                           "*[@onclick or @tabindex or @role='link' or @role='button']"];
  var xpath = [];
  for (var i in clickableElements)
    xpath.push("//" + clickableElements[i], "//xhtml:" + clickableElements[i]);
  return xpath.join(" | ")
})();

// We need this as a top-level function because our command system doesn't yet support arguments.
function activateLinkHintsModeToOpenInNewTab() { activateLinkHintsMode(true, false); }

function activateLinkHintsModeWithQueue() { activateLinkHintsMode(true, true); }

function activateLinkHintsMode(openInNewTab, withQueue) {
  if (!linkHintsCssAdded)
    addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js
  linkHintCssAdded = true;
  linkHintsModeActivated = true;
  setOpenLinkMode(openInNewTab, withQueue);
  buildLinkHints();
  document.addEventListener("keydown", onKeyDownInLinkHintsMode, true);
  document.addEventListener("keyup", onKeyUpInLinkHintsMode, true);
}

function setOpenLinkMode(openInNewTab, withQueue) {
  shouldOpenLinkHintInNewTab = openInNewTab;
  shouldOpenLinkHintWithQueue = withQueue;
  if (shouldOpenLinkHintWithQueue) {
    HUD.show("Open multiple links in a new tab");
  } else {
    if (shouldOpenLinkHintInNewTab)
      HUD.show("Open link in new tab");
    else
      HUD.show("Open link in current tab");
  }
}

/*
 * Builds and displays link hints for every visible clickable item on the page.
 */
function buildLinkHints() {
  var visibleElements = getVisibleClickableElements();

  // Initialize the number used to generate the character hints to be as many digits as we need to
  // highlight all the links on the page; we don't want some link hints to have more chars than others.
  var digitsNeeded = Math.ceil(logXOfBase(visibleElements.length, settings.linkHintCharacters.length));
  var linkHintNumber = 0;
  for (var i = 0; i < visibleElements.length; i++) {
    hintMarkers.push(createMarkerFor(visibleElements[i], linkHintNumber, digitsNeeded));
    linkHintNumber++;
  }
  // Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
  // because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
  // that if you scroll the page and the link has position=fixed, the marker will not stay fixed.
  // Also note that adding these nodes to document.body all at once is significantly faster than one-by-one.
  hintMarkerContainingDiv = document.createElement("div");
  hintMarkerContainingDiv.className = "internalVimiumHintMarker";
  for (var i = 0; i < hintMarkers.length; i++)
    hintMarkerContainingDiv.appendChild(hintMarkers[i]);
  document.body.appendChild(hintMarkerContainingDiv);
}

function logXOfBase(x, base) { return Math.log(x) / Math.log(base); }

/*
 * Returns all clickable elements that are not hidden and are in the current viewport.
 * We prune invisible elements partly for performance reasons, but moreso it's to decrease the number
 * of digits needed to enumerate all of the links on screen.
 */
function getVisibleClickableElements() {
  var resultSet = document.evaluate(clickableElementsXPath, document.body,
    function (namespace) {
      return namespace == "xhtml" ? "http://www.w3.org/1999/xhtml" : null;
    },
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  var visibleElements = [];

  // Find all visible clickable elements.
  for (var i = 0; i < resultSet.snapshotLength; i++) {
    var element = resultSet.snapshotItem(i);
    var clientRect = element.getClientRects()[0];

    if (isVisible(element, clientRect))
      visibleElements.push({element: element, rect: clientRect});

    // If the link has zero dimensions, it may be wrapping visible
    // but floated elements. Check for this.
    if (clientRect && (clientRect.width == 0 || clientRect.height == 0)) {
      for (var j = 0; j < element.children.length; j++) {
        if (window.getComputedStyle(element.children[j], null).getPropertyValue('float') != 'none') {
          var childClientRect = element.children[j].getClientRects()[0];
          if (isVisible(element.children[j], childClientRect)) {
            visibleElements.push({element: element.children[j], rect: childClientRect});
            break;
          }
        }
      }
    }
  }
  return visibleElements;
}

/*
 * Returns true if element is visible.
 */
function isVisible(element, clientRect) {
  // Exclude links which have just a few pixels on screen, because the link hints won't show for them anyway.
  var zoomFactor = currentZoomLevel / 100.0;
  if (!clientRect || clientRect.top < 0 || clientRect.top * zoomFactor >= window.innerHeight - 4 ||
      clientRect.left < 0 || clientRect.left * zoomFactor >= window.innerWidth - 4)
    return false;

  if (clientRect.width < 3 || clientRect.height < 3)
    return false;

  // eliminate invisible elements (see test_harnesses/visibility_test.html)
  var computedStyle = window.getComputedStyle(element, null);
  if (computedStyle.getPropertyValue('visibility') != 'visible' ||
      computedStyle.getPropertyValue('display') == 'none')
    return false;

  return true;
}

function onKeyDownInLinkHintsMode(event) {
  console.log("Key Down");
  if (event.keyCode == keyCodes.shiftKey && !openLinkModeToggle) {
    // Toggle whether to open link in a new or current tab.
    setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
    openLinkModeToggle = true;
  }

  var keyChar = getKeyChar(event);
  if (!keyChar)
    return;

  // TODO(philc): Ignore keys that have modifiers.
  if (isEscape(event)) {
    deactivateLinkHintsMode();
  } else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
    if (hintKeystrokeQueue.length == 0) {
      deactivateLinkHintsMode();
    } else {
      hintKeystrokeQueue.pop();
      updateLinkHints();
    }
  } else if (settings.linkHintCharacters.indexOf(keyChar) >= 0) {
    hintKeystrokeQueue.push(keyChar);
    updateLinkHints();
  } else {
    return;
  }

  event.stopPropagation();
  event.preventDefault();
}

function onKeyUpInLinkHintsMode(event) {
  if (event.keyCode == keyCodes.shiftKey && openLinkModeToggle) {
    // Revert toggle on whether to open link in new or current tab.
    setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
    openLinkModeToggle = false;
  }
  event.stopPropagation();
  event.preventDefault();
}

/*
 * Updates the visibility of link hints on screen based on the keystrokes typed thus far. If only one
 * link hint remains, click on that link and exit link hints mode.
 */
function updateLinkHints() {
  var matchString = hintKeystrokeQueue.join("");
  var linksMatched = highlightLinkMatches(matchString);
  if (linksMatched.length == 0)
    deactivateLinkHintsMode();
  else if (linksMatched.length == 1) {
    var matchedLink = linksMatched[0];
    if (isSelectable(matchedLink)) {
      matchedLink.focus();
      // When focusing a textbox, put the selection caret at the end of the textbox's contents.
      matchedLink.setSelectionRange(matchedLink.value.length, matchedLink.value.length);
      deactivateLinkHintsMode();
    } else {
      // When we're opening the link in the current tab, don't navigate to the selected link immediately;
      // we want to give the user some feedback depicting which link they've selected by focusing it.
      if (shouldOpenLinkHintWithQueue) {
        simulateClick(matchedLink);
        resetLinkHintsMode();
      } else if (shouldOpenLinkHintInNewTab) {
        simulateClick(matchedLink);
        matchedLink.focus();
        deactivateLinkHintsMode();
      } else {
        setTimeout(function() { simulateClick(matchedLink); }, 400);
        matchedLink.focus();
        deactivateLinkHintsMode();
      }
    }
  }
}

/*
 * Selectable means the element has a text caret; this is not the same as "focusable".
 */
function isSelectable(element) {
  var selectableTypes = ["search", "text", "password"];
  return (element.tagName == "INPUT" && selectableTypes.indexOf(element.type) >= 0) ||
      element.tagName == "TEXTAREA";
}

/*
 * Hides link hints which do not match the given search string. To allow the backspace key to work, this
 * will also show link hints which do match but were previously hidden.
 */
function highlightLinkMatches(searchString) {
  var linksMatched = [];
  for (var i = 0; i < hintMarkers.length; i++) {
    var linkMarker = hintMarkers[i];
    if (linkMarker.getAttribute("hintString").indexOf(searchString) == 0) {
      if (linkMarker.style.display == "none")
        linkMarker.style.display = "";
      for (var j = 0; j < linkMarker.childNodes.length; j++)
        linkMarker.childNodes[j].className = (j >= searchString.length) ? "" : "matchingCharacter";
      linksMatched.push(linkMarker.clickableItem);
    } else {
      linkMarker.style.display = "none";
    }
  }
  return linksMatched;
}

/*
 * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of
 * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits.
 */
function numberToHintString(number, numHintDigits) {
  var base = settings.linkHintCharacters.length;
  var hintString = [];
  var remainder = 0;
  do {
    remainder = number % base;
    hintString.unshift(settings.linkHintCharacters[remainder]);
    number -= remainder;
    number /= Math.floor(base);
  } while (number > 0);

  // Pad the hint string we're returning so that it matches numHintDigits.
  var hintStringLength = hintString.length;
  for (var i = 0; i < numHintDigits - hintStringLength; i++)
    hintString.unshift(settings.linkHintCharacters[0]);
  return hintString.join("");
}

function simulateClick(link) {
  var event = document.createEvent("MouseEvents");
  // When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on windows)
  // to open it in a new tab if necessary.
  var metaKey = (platform == "Mac" && shouldOpenLinkHintInNewTab);
  var ctrlKey = (platform != "Mac" && shouldOpenLinkHintInNewTab);
  event.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null);

  // Debugging note: Firefox will not execute the link's default action if we dispatch this click event,
  // but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
  link.dispatchEvent(event);
}

function deactivateLinkHintsMode() {
  if (hintMarkerContainingDiv)
    hintMarkerContainingDiv.parentNode.removeChild(hintMarkerContainingDiv);
  hintMarkerContainingDiv = null;
  hintMarkers = [];
  hintKeystrokeQueue = [];
  document.removeEventListener("keydown", onKeyDownInLinkHintsMode, true);
  document.removeEventListener("keyup", onKeyUpInLinkHintsMode, true);
  linkHintsModeActivated = false;
  HUD.hide();
}

function resetLinkHintsMode() {
  deactivateLinkHintsMode();
  activateLinkHintsModeWithQueue();
}

/*
 * Creates a link marker for the given link.
 */
function createMarkerFor(link, linkHintNumber, linkHintDigits) {
  var hintString = numberToHintString(linkHintNumber, linkHintDigits);
  var marker = document.createElement("div");
  marker.className = "internalVimiumHintMarker vimiumHintMarker";
  var innerHTML = [];
  // Make each hint character a span, so that we can highlight the typed characters as you type them.
  for (var i = 0; i < hintString.length; i++)
    innerHTML.push("<span>" + hintString[i].toUpperCase() + "</span>");
  marker.innerHTML = innerHTML.join("");
  marker.setAttribute("hintString", hintString);

  // Note: this call will be expensive if we modify the DOM in between calls.
  var clientRect = link.rect;
  // The coordinates given by the window do not have the zoom factor included since the zoom is set only on
  // the document node.
  var zoomFactor = currentZoomLevel / 100.0;
  marker.style.left = clientRect.left + window.scrollX / zoomFactor + "px";
  marker.style.top = clientRect.top  + window.scrollY / zoomFactor + "px";

  marker.clickableItem = link.element;
  return marker;
}
<|endoftext|>


<commit_before>// index.js ~ Copyright 2015 Paul Beaudet ~ MIT License see LICENSE_MIT for detials
// Constants, these can change the behavior of the application
// Timing constants
var MINUTE = 60000      // 60 seconds
var EXPIRE_CHECK = 5000 // 5 seconds
var WAIT_TIME = 30;     // time to wait for talking turn
var TOPIC_TIME = 60;    // time for topics to show
var NUM_ENTRIES = 6;    // number of dialog rows allowed in the application
var NUM_TIMERS = NUM_ENTRIES + 1; // timer 6 is for the send button
// call NUM_ENTRIES for send button timer
var SEND_TIMER = NUM_ENTRIES;
var TIMER_TEXT_SEND = 'T -';
// Typing modes
var TOPIC = 0; // users post or select topics
var CHAT  = 1; // two users chat one on one
var BLOCK = 2; // blocks user entry, waiting for either their topic to expire or chatting partner to finish

// Methods revolving around tracking possition of row
var edit = { // dep: time, textBar, $
    row: 1,
    increment: function(){ // decides with row to edit to and when the dialog needs to scoot up
        if(edit.row < NUM_ENTRIES - 2)         { edit.row++; }
        else if ( edit.row === NUM_ENTRIES - 2){ edit.row = NUM_ENTRIES - 1; }
        else if ( edit.row < NUM_ENTRIES + 1)  { edit.scoot(); }
    },
    onStart: function(){ // called when starting a message
        if(edit.row === NUM_ENTRIES){
            edit.scoot();
            edit.row--;
        }  // checks to see if a scoot is needed upon typing
        time.from(edit.row, "You");
    },
    type: function(data){
        var thisRow = 0;
        if(data.row){thisRow = data.row;} else{thisRow = edit.row;}
        $('#dialog' + thisRow).html(data.text);
    },
    myTurn: function(){
        send.mode = CHAT;             // allow user to type
        textBar.changeAction(CHAT);   // clear text box and use correct dialog
        time.from(edit.row, "other"); // write other onto the last row
        edit.increment();             // increment place to write to
        time.countDown(SEND_TIMER, TIMER_TEXT_SEND, send.passOn);     // time out input
    },
    scoot: function(){
        for(var i = 2; i < NUM_ENTRIES; i++){
            $( '#dialog' + ( i - 1 ) ).html( $('#dialog' + i).html() );
            $( '#timer' + ( i - 1 ) ).html( $('#timer' + i).html() );
        }
        $( '#dialog' + (NUM_ENTRIES - 1) ).html('');
        $( '#timer' + (NUM_ENTRIES -1) ).html('');
    }
}

// changeition handling of visual elements
var change = { // dep: send, time, textBar
    toHome: function(){
        time.clear();
        for(var i = 0; i < NUM_ENTRIES; i++){                     // for every entry
            if(i){$('#dialog' + i).html('');}                     // set dialog to be empty
            else{$('#dialog' + i).html('Waiting for topics...');} // set a default entry for dialog 0
            $('#button' + i).css('visibility', 'hidden');         //  hide buttons
        }
        send.mode = TOPIC; // reset into topic mode
        info.home();       // Set hint text for topic picking mode
        edit.row = 0;      // topic can now posted to possition 0
        textBar.changeAction(send.mode); // make sure send mode is properly set before change.home called
    },
    toChat: function(data){
        send.to = data.id;                                    // set who topics are being sent to
        time.clear();                                         // clear outstanding timers
        info.chat();                                          // set informational dialog
        edit.row = 1;                                         // make sure editing starts on the correct row
        for(var row = 0; row < NUM_ENTRIES; row++){           // remove everything that was on topic screen
            $('#button' + row).css('visibility', 'hidden');   // hide buttons
            $('#icon' + row).removeClass('glyphicon-remove'); // reset type of icon
            $('#icon' + row).removeClass('glyphicon-plus');   // reset type of icon
            if(row){$('#dialog' + row).html('');}             // clear dialog
            else{$('#dialog0').html(topic.pending);}          // set topic to first dialog item
        }
        if(data.first){
            send.mode = CHAT;
            time.from(1, "You");
            time.countDown(SEND_TIMER, TIMER_TEXT_SEND, send.passOn); // time out input
        } else {
            send.mode = BLOCK;
            time.from(1, "other");
        }
        textBar.changeAction(send.mode);
    },
}

var CHATGLYPH = 'user'; // || plus || remove
var SUBGLYPH = 'plus';

// logic for recieving topics to subscribe to and matched topics
var topic = { // dep: time, $
    pending: '',
    firstEmpty: function(){
        for( var i=0; i < NUM_ENTRIES; i++){
            if($('#icon' + i).hasClass('glyphicon-' + SUBGLYPH) || $('#icon' + i).hasClass('glyphicon-' + CHATGLYPH) ){;}
            else{return i;} // given nothing in this row signal it is free
        } // if the rows have been exhusted write to first row
        return 0;
    },
    get: function(get){
        for(var i = 0; i < NUM_ENTRIES; i++){if(get.text === $('#dialog' + row).html()){return;}} // reject if existing
        var row = topic.firstEmpty();                                     // note first available row
        if($.type(get.user) === 'string'){                                // detect match situation
            $('#icon' + row).addClass('glyphicon-' + CHATGLYPH);          // add "decline" icon
            time.countDown(row, 'Cancel -', function(){topic.start(row, get.user);}); // Set timer on this row
        } else {                                                          // given subbable
            $('#icon' + row).addClass('glyphicon-' + SUBGLYPH);           // add the plus icon
            $('#button' + row).click(function(){                          // If sub button is clicked
                sock.et.emit('sub', get.user);                            // subscribe event
                inactivity.status = false;                                // note activity
                topic.done(row, SUBGLYPH);
            });
            time.countDown(row, 'Add -',function(){topic.done(row, SUBGLYPH)});     // Set timer on this row
        }
        $('#button' + row).css('visibility', 'visible');
        $('#dialog' + row).html(get.text);
    },
    done: function(row, icon) {                            // action to occur on count end, removes entry
        clearTimeout(time.inProg[row]);                    // deactivate timeout if active
        time.counter[row] = WAIT_TIME;                     // reset timer
        $('#icon' + row).removeClass('glyphicon-' + icon); // reset so sub or decline can be reintroduced
        $('#button' + row).off('click');                   // remove click event
        $('#button' + row).css('visibility', 'hidden');    // on end hide button
        $('#dialog' + row).html('');                       // on end remove dialog
    },
    start: function(row, talkingTo){               // at this juncture we will need to start the conversation
        topic.pending = $('#dialog' + row).html(); // store topic dialog
        topic.done(row, CHATGLYPH);                // remove this item from list
        sock.et.emit("initTopic", talkingTo);      // signal which user that needs to be connected with
    }
}

// sending logic
var send = { // dep: sock, change, edit, textBar
    empty: true,
    mode: 0,
    to: '', // potential user id
    nonPrint: function(event){ // account for pressing the enter key
        if(send.mode === TOPIC || send.mode === CHAT){if(event.which === 13){send.passOn();}}
    },
    passOn: function(){
        if(send.mode === TOPIC){
            send.create(); // start ttl timer, shows time topics have to live in send timer
        } else if (send.mode === CHAT){
            if(send.empty){
                sock.et.emit('endChat', send.to);
                change.toHome();     // trasition back to home screen
            } else {
                sock.et.emit('toOther', send.to);
                edit.increment();            // increase row number to edit
                send.mode = BLOCK;           // block user till other responds
                textBar.changeAction(BLOCK); // show wait notice
                time.stopSend("");           // stop the clock from running anymore
            }
        }
        inactivity.status = false; // note activity
        send.empty = true;         // it will be empty when it is responded to.
    },
    input: function(){
        if(send.mode === TOPIC){
            if(send.empty){
                send.empty = false;
                time.counter[SEND_TIMER] = WAIT_TIME - 1; // note: Make sure post sent before timeout on other client
                time.countDown(SEND_TIMER, TIMER_TEXT_SEND, send.create);
                // this is where breakers will start being timed
            }
        }else if(send.mode === CHAT){
            if(send.empty){edit.onStart(); send.empty = false;} // account for nessisary changeitions
            edit.type({text: $('#textEntry').val(), row: 0});     // print on own screen
            sock.et.emit("chat", {text: $('#textEntry').val(), id: send.to}); // send to other user
        } // block more input from happening, leaving last sent message in box
        else if(send.mode === BLOCK){ $('#textEntry').val(''); }
        inactivity.status = false; // note activity
    },
    create: function(){ // called when topic composition is complete
        sock.et.emit('create', $('#textEntry').val()); // Signal to the server that composition of topic is done
        time.stopSend(WAIT_TIME);        // in case this was called by passOn
        send.mode = BLOCK;               // block input till time to live is over
        textBar.changeAction(BLOCK);     // display notice of block
        time.countDown(SEND_TIMER, TIMER_TEXT_SEND, function(){
            time.stopSend("");           // reset timer
            send.empty = true;           // text is now empty
            send.mode = TOPIC;           // set so topics can be made again
            textBar.changeAction(TOPIC); // display notice that topics can be made again
        });
    }
}

// -- socket handler
var sock = {  // dep: sockets.io, topic, change, edit, send
    et: io(), // connect to server the page was served from
    init: function (){                         // Topic starting components
        sock.et.on('topic', topic.get);        // grab time to live topics: timed from the getgo
        sock.et.on('chatInit', change.toChat); // someone wants to chat with us
                                               // real time chat reception components
        sock.et.on('toMe', edit.type);         // recieve Real Time Text
        sock.et.on('yourTurn', edit.myTurn);   // signals when it is this clients turn to type
        sock.et.on('endChat', change.toHome);  // switch back to default appearence

        sock.et.on('redirect', window.location.replace); // redirect to desired page
    }
}

// informational header, this can change from screen to screen to indicate use information
var info = {
    inProg: null,
    chat: function(){
        $('#info').html('Chat: Pressing done while text box empty, ends chat');
        info.inProg = setTimeout(function(){
            $('#info').html('Actions auto-complete on timeout');
        }, (WAIT_TIME / 2 * 1000));
    },
    home: function(){
        if(info.inProg){clearTimeout(info.inProg);}
        $('#info').html('Subscribe to topics or wait for conversation');
    }
}

// session logic -- Logs out user if inactive
var inactivity = {
    accumalated: 0,
    status: true,
    check: function(){
        if(inactivity.status){ inactivity.accumalated += EXPIRE_CHECK; }
        else { inactivity.accumalated = 0; }
        if(inactivity.accumalated > MINUTE){   // if inactivitly continues beyound threshold
            window.location.replace('/login'); // redirect to login page
        } else {
            setTimeout(inactivity.check, EXPIRE_CHECK);
        }
        inactivity.status = true;
    }
}

// timer logic
var time = { // dep: document
    counter: [], // one exist per row, last is send timer
    inProg: [],  // IDs of timers in case clear is needed
    countDown: function(row, text, ondone){
        if (time.counter[row]) {
            time.counter[row]--;
            $('#timer' + row).html(text + time.counter[row] + " ");
            time.inProg[row] = setTimeout(function(){time.countDown(row, text, ondone);}, 1000);
        } else {
            $('#timer' + row).html('');
            time.counter[row] = WAIT_TIME;
            if(ondone){ondone();}
        }
    },
    clear: function(){
        for (var i = 0; i < NUM_TIMERS; i++){
            if(time.inProg[i]){clearTimeout(time.inProg[i]);} // deactivate active timeouts
            $('#timer' + i).html('');                         // empty timer text
            time.counter[i] = WAIT_TIME;                      // reset timeouts
        }
    },
    stopSend: function(text){
        clearTimeout(time.inProg[SEND_TIMER]);
        $('#timer' + SEND_TIMER).html(text);
        time.counter[SEND_TIMER] = WAIT_TIME;
    },
    from: function(whichRow, who){ // replaces time span elemement with perspective of user
        $('#timer' + whichRow).css('visibility', 'visible');
        $('#timer' + whichRow).html(who);               // which perspective is this
    }, // Should probably be done with a seperate element but for the sake of simplicity this one is reused
}

// Bottom of page text bar footer object
var textBar = { // dep: $
    changeAction: function(mode){
        if(mode === TOPIC){
            $('#sendText').html('Make Topic ');
            $('#textEntry').val('');
        } else if ( mode === CHAT ){
            $('#sendText').html('Done');
            $('#textEntry').val('');
        } else if ( mode === BLOCK){
            $('#sendText').html('Wait ');
        }
    }
}

// -- Global execution -- Set up application on DOM ready
$(document).ready(function(){
    change.toHome();                                           // default appearance
    $('#textEntry').keydown(send.nonPrint);                    // capture special key like enter
    $('#sendButton').click(send.passOn);                       // provide a button click action
    document.getElementById('textEntry').oninput = send.input; // listen for input event
    sock.init();                                               // listen for socket events
    inactivity.check();                                        // checks for session inactivity
});
<commit_msg>Remember to actually iterate through something<commit_after>// index.js ~ Copyright 2015 Paul Beaudet ~ MIT License see LICENSE_MIT for detials
// Constants, these can change the behavior of the application
// Timing constants
var MINUTE = 60000      // 60 seconds
var EXPIRE_CHECK = 5000 // 5 seconds
var WAIT_TIME = 30;     // time to wait for talking turn
var TOPIC_TIME = 60;    // time for topics to show
var NUM_ENTRIES = 6;    // number of dialog rows allowed in the application
var NUM_TIMERS = NUM_ENTRIES + 1; // timer 6 is for the send button
// call NUM_ENTRIES for send button timer
var SEND_TIMER = NUM_ENTRIES;
var TIMER_TEXT_SEND = 'T -';
// Typing modes
var TOPIC = 0; // users post or select topics
var CHAT  = 1; // two users chat one on one
var BLOCK = 2; // blocks user entry, waiting for either their topic to expire or chatting partner to finish

// Methods revolving around tracking possition of row
var edit = { // dep: time, textBar, $
    row: 1,
    increment: function(){ // decides with row to edit to and when the dialog needs to scoot up
        if(edit.row < NUM_ENTRIES - 2)         { edit.row++; }
        else if ( edit.row === NUM_ENTRIES - 2){ edit.row = NUM_ENTRIES - 1; }
        else if ( edit.row < NUM_ENTRIES + 1)  { edit.scoot(); }
    },
    onStart: function(){ // called when starting a message
        if(edit.row === NUM_ENTRIES){
            edit.scoot();
            edit.row--;
        }  // checks to see if a scoot is needed upon typing
        time.from(edit.row, "You");
    },
    type: function(data){
        var thisRow = 0;
        if(data.row){thisRow = data.row;} else{thisRow = edit.row;}
        $('#dialog' + thisRow).html(data.text);
    },
    myTurn: function(){
        send.mode = CHAT;             // allow user to type
        textBar.changeAction(CHAT);   // clear text box and use correct dialog
        time.from(edit.row, "other"); // write other onto the last row
        edit.increment();             // increment place to write to
        time.countDown(SEND_TIMER, TIMER_TEXT_SEND, send.passOn);     // time out input
    },
    scoot: function(){
        for(var i = 2; i < NUM_ENTRIES; i++){
            $( '#dialog' + ( i - 1 ) ).html( $('#dialog' + i).html() );
            $( '#timer' + ( i - 1 ) ).html( $('#timer' + i).html() );
        }
        $( '#dialog' + (NUM_ENTRIES - 1) ).html('');
        $( '#timer' + (NUM_ENTRIES -1) ).html('');
    }
}

// changeition handling of visual elements
var change = { // dep: send, time, textBar
    toHome: function(){
        time.clear();
        for(var i = 0; i < NUM_ENTRIES; i++){                     // for every entry
            if(i){$('#dialog' + i).html('');}                     // set dialog to be empty
            else{$('#dialog' + i).html('Waiting for topics...');} // set a default entry for dialog 0
            $('#button' + i).css('visibility', 'hidden');         //  hide buttons
        }
        send.mode = TOPIC; // reset into topic mode
        info.home();       // Set hint text for topic picking mode
        edit.row = 0;      // topic can now posted to possition 0
        textBar.changeAction(send.mode); // make sure send mode is properly set before change.home called
    },
    toChat: function(data){
        send.to = data.id;                                    // set who topics are being sent to
        time.clear();                                         // clear outstanding timers
        info.chat();                                          // set informational dialog
        edit.row = 1;                                         // make sure editing starts on the correct row
        for(var row = 0; row < NUM_ENTRIES; row++){           // remove everything that was on topic screen
            $('#button' + row).css('visibility', 'hidden');   // hide buttons
            $('#icon' + row).removeClass('glyphicon-remove'); // reset type of icon
            $('#icon' + row).removeClass('glyphicon-plus');   // reset type of icon
            if(row){$('#dialog' + row).html('');}             // clear dialog
            else{$('#dialog0').html(topic.pending);}          // set topic to first dialog item
        }
        if(data.first){
            send.mode = CHAT;
            time.from(1, "You");
            time.countDown(SEND_TIMER, TIMER_TEXT_SEND, send.passOn); // time out input
        } else {
            send.mode = BLOCK;
            time.from(1, "other");
        }
        textBar.changeAction(send.mode);
    },
}

var CHATGLYPH = 'user'; // || plus || remove
var SUBGLYPH = 'plus';

// logic for recieving topics to subscribe to and matched topics
var topic = { // dep: time, $
    pending: '',
    firstEmpty: function(){
        for( var i=0; i < NUM_ENTRIES; i++){
            if($('#icon' + i).hasClass('glyphicon-' + SUBGLYPH) || $('#icon' + i).hasClass('glyphicon-' + CHATGLYPH) ){;}
            else{return i;} // given nothing in this row signal it is free
        } // if the rows have been exhusted write to first row
        return 0;
    },
    get: function(get){
        for(var i = 0; i < NUM_ENTRIES; i++){if(get.text === $('#dialog' + i).html()){return;}} // reject if existing
        var row = topic.firstEmpty();                                     // note first available row
        if($.type(get.user) === 'string'){                                // detect match situation
            $('#icon' + row).addClass('glyphicon-' + CHATGLYPH);          // add "decline" icon
            time.countDown(row, 'Cancel -', function(){topic.start(row, get.user);}); // Set timer on this row
        } else {                                                          // given subbable
            $('#icon' + row).addClass('glyphicon-' + SUBGLYPH);           // add the plus icon
            $('#button' + row).click(function(){                          // If sub button is clicked
                sock.et.emit('sub', get.user);                            // subscribe event
                inactivity.status = false;                                // note activity
                topic.done(row, SUBGLYPH);
            });
            time.countDown(row, 'Add -',function(){topic.done(row, SUBGLYPH)});     // Set timer on this row
        }
        $('#button' + row).css('visibility', 'visible');
        $('#dialog' + row).html(get.text);
    },
    done: function(row, icon) {                            // action to occur on count end, removes entry
        clearTimeout(time.inProg[row]);                    // deactivate timeout if active
        time.counter[row] = WAIT_TIME;                     // reset timer
        $('#icon' + row).removeClass('glyphicon-' + icon); // reset so sub or decline can be reintroduced
        $('#button' + row).off('click');                   // remove click event
        $('#button' + row).css('visibility', 'hidden');    // on end hide button
        $('#dialog' + row).html('');                       // on end remove dialog
    },
    start: function(row, talkingTo){               // at this juncture we will need to start the conversation
        topic.pending = $('#dialog' + row).html(); // store topic dialog
        topic.done(row, CHATGLYPH);                // remove this item from list
        sock.et.emit("initTopic", talkingTo);      // signal which user that needs to be connected with
    }
}

// sending logic
var send = { // dep: sock, change, edit, textBar
    empty: true,
    mode: 0,
    to: '', // potential user id
    nonPrint: function(event){ // account for pressing the enter key
        if(send.mode === TOPIC || send.mode === CHAT){if(event.which === 13){send.passOn();}}
    },
    passOn: function(){
        if(send.mode === TOPIC){
            send.create(); // start ttl timer, shows time topics have to live in send timer
        } else if (send.mode === CHAT){
            if(send.empty){
                sock.et.emit('endChat', send.to);
                change.toHome();     // trasition back to home screen
            } else {
                sock.et.emit('toOther', send.to);
                edit.increment();            // increase row number to edit
                send.mode = BLOCK;           // block user till other responds
                textBar.changeAction(BLOCK); // show wait notice
                time.stopSend("");           // stop the clock from running anymore
            }
        }
        inactivity.status = false; // note activity
        send.empty = true;         // it will be empty when it is responded to.
    },
    input: function(){
        if(send.mode === TOPIC){
            if(send.empty){
                send.empty = false;
                time.counter[SEND_TIMER] = WAIT_TIME - 1; // note: Make sure post sent before timeout on other client
                time.countDown(SEND_TIMER, TIMER_TEXT_SEND, send.create);
                // this is where breakers will start being timed
            }
        }else if(send.mode === CHAT){
            if(send.empty){edit.onStart(); send.empty = false;} // account for nessisary changeitions
            edit.type({text: $('#textEntry').val(), row: 0});     // print on own screen
            sock.et.emit("chat", {text: $('#textEntry').val(), id: send.to}); // send to other user
        } // block more input from happening, leaving last sent message in box
        else if(send.mode === BLOCK){ $('#textEntry').val(''); }
        inactivity.status = false; // note activity
    },
    create: function(){ // called when topic composition is complete
        sock.et.emit('create', $('#textEntry').val()); // Signal to the server that composition of topic is done
        time.stopSend(WAIT_TIME);        // in case this was called by passOn
        send.mode = BLOCK;               // block input till time to live is over
        textBar.changeAction(BLOCK);     // display notice of block
        time.countDown(SEND_TIMER, TIMER_TEXT_SEND, function(){
            time.stopSend("");           // reset timer
            send.empty = true;           // text is now empty
            send.mode = TOPIC;           // set so topics can be made again
            textBar.changeAction(TOPIC); // display notice that topics can be made again
        });
    }
}

// -- socket handler
var sock = {  // dep: sockets.io, topic, change, edit, send
    et: io(), // connect to server the page was served from
    init: function (){                         // Topic starting components
        sock.et.on('topic', topic.get);        // grab time to live topics: timed from the getgo
        sock.et.on('chatInit', change.toChat); // someone wants to chat with us
                                               // real time chat reception components
        sock.et.on('toMe', edit.type);         // recieve Real Time Text
        sock.et.on('yourTurn', edit.myTurn);   // signals when it is this clients turn to type
        sock.et.on('endChat', change.toHome);  // switch back to default appearence

        sock.et.on('redirect', window.location.replace); // redirect to desired page
    }
}

// informational header, this can change from screen to screen to indicate use information
var info = {
    inProg: null,
    chat: function(){
        $('#info').html('Chat: Pressing done while text box empty, ends chat');
        info.inProg = setTimeout(function(){
            $('#info').html('Actions auto-complete on timeout');
        }, (WAIT_TIME / 2 * 1000));
    },
    home: function(){
        if(info.inProg){clearTimeout(info.inProg);}
        $('#info').html('Subscribe to topics or wait for conversation');
    }
}

// session logic -- Logs out user if inactive
var inactivity = {
    accumalated: 0,
    status: true,
    check: function(){
        if(inactivity.status){ inactivity.accumalated += EXPIRE_CHECK; }
        else { inactivity.accumalated = 0; }
        if(inactivity.accumalated > MINUTE){   // if inactivitly continues beyound threshold
            window.location.replace('/login'); // redirect to login page
        } else {
            setTimeout(inactivity.check, EXPIRE_CHECK);
        }
        inactivity.status = true;
    }
}

// timer logic
var time = { // dep: document
    counter: [], // one exist per row, last is send timer
    inProg: [],  // IDs of timers in case clear is needed
    countDown: function(row, text, ondone){
        if (time.counter[row]) {
            time.counter[row]--;
            $('#timer' + row).html(text + time.counter[row] + " ");
            time.inProg[row] = setTimeout(function(){time.countDown(row, text, ondone);}, 1000);
        } else {
            $('#timer' + row).html('');
            time.counter[row] = WAIT_TIME;
            if(ondone){ondone();}
        }
    },
    clear: function(){
        for (var i = 0; i < NUM_TIMERS; i++){
            if(time.inProg[i]){clearTimeout(time.inProg[i]);} // deactivate active timeouts
            $('#timer' + i).html('');                         // empty timer text
            time.counter[i] = WAIT_TIME;                      // reset timeouts
        }
    },
    stopSend: function(text){
        clearTimeout(time.inProg[SEND_TIMER]);
        $('#timer' + SEND_TIMER).html(text);
        time.counter[SEND_TIMER] = WAIT_TIME;
    },
    from: function(whichRow, who){ // replaces time span elemement with perspective of user
        $('#timer' + whichRow).css('visibility', 'visible');
        $('#timer' + whichRow).html(who);               // which perspective is this
    }, // Should probably be done with a seperate element but for the sake of simplicity this one is reused
}

// Bottom of page text bar footer object
var textBar = { // dep: $
    changeAction: function(mode){
        if(mode === TOPIC){
            $('#sendText').html('Make Topic ');
            $('#textEntry').val('');
        } else if ( mode === CHAT ){
            $('#sendText').html('Done');
            $('#textEntry').val('');
        } else if ( mode === BLOCK){
            $('#sendText').html('Wait ');
        }
    }
}

// -- Global execution -- Set up application on DOM ready
$(document).ready(function(){
    change.toHome();                                           // default appearance
    $('#textEntry').keydown(send.nonPrint);                    // capture special key like enter
    $('#sendButton').click(send.passOn);                       // provide a button click action
    document.getElementById('textEntry').oninput = send.input; // listen for input event
    sock.init();                                               // listen for socket events
    inactivity.check();                                        // checks for session inactivity
});
<|endoftext|>


<commit_before><commit_msg>Shouldn't need that<commit_after><|endoftext|>


<commit_before><commit_msg>Update backbone to use commonJS<commit_after><|endoftext|>