<commit_before>BEST.component('famous-ads:fitbit', {
    tree: 'fitbit.html',
    behaviors: {
        '#ad-container': {
            'size': function(adSize) {
                return adSize;
            }
        },
        '#container-background': {
            'style': {
                'background-color': 'white',
                'z-index': '-10'
            }
        },
        '#meet-family-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [20, 50]],
                    [750,   [20, 50]],
                    [1000,  [1000, 50], 'outExpo']
                ])(time);
            },
        },
        '#meet-family': {
            'true-size': true,
            'style': {
                'color': 'rgb(71, 187, 179)',
                'font-weight': '800',
                'font-size': '35px',
                'text-align': 'center'
            }
        },
        '#bottom-bar-container': {
            'size': function(bottomBarSize) {
                return bottomBarSize;
            },
            'position': function(bottomBarPosition) {
                return bottomBarPosition;
            }
        },
        '#bottom-bar-background': {
            'style': {
                'background-color': 'rgb(17, 36, 41)',
                'z-index': '-5'
            }
        },
        '#fitbit-logo-container': {
            'position': function(fitbitLogoPosition) {
                return fitbitLogoPosition;
            }
        },
        '#shop-now-container': {
            'size': function(shopNowSize) {
                return shopNowSize;
            },
            'position': function(shopNowPosition) {
                return shopNowPosition;
            }
        },
        '#shop-now': {
            'style': function(shopNowBackgroundColor, shopNowTextColor) {
                return {
                    'background-color': shopNowBackgroundColor,
                    'color': shopNowTextColor,
                    'border-radius': '10px',
                    'text-align': 'center',
                    'font-size': '18px',
                    'transition': 'all 0.25s ease'
                }
            }
        },
        '#charge-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 60]],
                    [750,   [-500, 60]],
                    [1000,  [180,  60], 'easeOut']
                ])(time);
            }
        },
        '#charge-img': {
            'true-size': true
        },
        '#charge-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [750,   [-500, 25]],
                    [1000,  [210,  25], 'easeOut'],
                    [5000,  [210,  25]],
                    [5500,  [1210, 25], 'easeOut'],
                    [5501,  [210,  25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, chargeRotationX) {
                return time > 5501 ? chargeRotationX :
                    _timeline([
                        [0,     0],
                        [5500,  0],
                        [5501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#charge-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500',
            }
        },
        '#flex-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 65]],
                    [1500,  [-500, 65]],
                    [2000,  [110,  65], 'easeOut'],
                ])(time);
            },

        },
        '#flex-img': {
            'true-size': true
        },
        '#flex-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [1500,  [-500, 25]],
                    [2000,  [140,  25], 'easeOut'],
                    [5000,  [140,  25]],
                    [5500,  [1140, 25], 'easeOut'],
                    [5501,  [140,  25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, flexRotationX) {
                return time > 5501 ? flexRotationX :
                    _timeline([
                        [0,     0],
                        [5500,  0],
                        [5501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#flex-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500',
            }
        },
        '#one-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 82]],
                    [2500,  [-500, 82]],
                    [3000,  [60,   82], 'easeOut']
                ])(time);
            }
        },
        '#one-img': {
            'true-size': true
        },
        '#one-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [2500,  [-500, 25]],
                    [3000,  [70,   25], 'easeOut'],
                    [5000,  [70,   25]],
                    [5500,  [1070, 25], 'easeOut'],
                    [5501,  [70,   25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, oneRotationX) {
                return time > 5501 ? oneRotationX :
                    _timeline([
                        [0,     0],
                        [5500,  0],
                        [5501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#one-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500'
            }
        },
        '#zip-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 105]],
                    [3500,  [-500, 105]],
                    [4000,  [0,    105], 'easeOut'],
                ])(time);
            }
        },
        '#zip-img': {
            'true-size': true
        },
        '#zip-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [3500,  [-500, 25]],
                    [4000,  [20,   25], 'easeOut'],
                    [5000,  [20,   25]],
                    [5500,  [1020, 25], 'easeOut'],
                    [5501,  [20,   25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, zipRotationX) {
                return time > 5501 ? zipRotationX :
                    _timeline([
                        [0,     0],
                        [5500,  0],
                        [5501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#zip-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500'
            }
        },
        '#find-fit-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-1000, 0]],
                    [5000,  [-1000, 0]],
                    [5250,  [75,    10], 'easeOut']
                ])(time);
            },
            'rotation-x': function(findFitRotationX) {
                return findFitRotationX;
            },
            'origin': [0.5, 0.5]
        },
        '#find-fit': {
            'true-size': true,
            'style': {
                'color': 'rgb(71, 187, 179)',
                'font-weight': '800',
                'font-size': '30px',
                'text-align': 'center'
            }
        },
        '$self': {
            '$self:startAd' : function(startAd) {
                return true;
            }
        }
    },
    events: {
        public: {
            'handle-charge-hover': function(state) {
                state
                    .set('chargeRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('flexRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(94, 122, 136)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-flex-hover': function(state) {
                state
                    .set('flexRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('chargeRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(232, 87, 60)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-one-hover': function(state) {
                state
                    .set('oneRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('chargeRotationX', Math.PI/2)
                    .set('flexRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(6, 6, 6)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-zip-hover': function(state) {
                state
                    .set('zipRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('chargeRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('flexRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(184, 222, 61)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-exit-hover': function(state) {
                state
                    .set('chargeRotationX', Math.PI/2)
                    .set('flexRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('shopNowBackgroundColor', 'rgb(239, 61, 111)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-button-hover': function(state) {
                state
                    .set('shopNowBackgroundColor', 'rgb(255, 255, 255)')
                    .set('shopNowTextColor', 'rgb(239, 61, 111)');
            }
        },
        handlers: {
            'startAd' : function($state, $payload) {
                $state.set('time', 10000, {duration: 10000});
            }
        }
    },
    states: {
        adSize: [300, 250],
        meetFamilyPosition: [20, 50],
        bottomBarSize: [300, 45],
        bottomBarPosition: [0, 205],
        fitbitLogoPosition: [15, 10],
        shopNowSize: [140, 28],
        shopNowPosition: [150, 10],
        shopNowBackgroundColor: 'rgb(239, 61, 111)',
        shopNowTextColor: 'rgb(255, 255, 255)',
        chargeImagePosition: [180, 60],
        chargeLabelPosition: [220, 30],
        flexImagePosition: [110, 65],
        flexLabelPosition: [145, 30],
        oneImagePosition: [60, 82],
        oneLabelPosition: [75, 30],
        zipImagePosition: [0, 105],
        zipLabelPosition: [30, 30],
        findFitPosition: [75, 10],
        findFitRotationX: 0,
        chargeRotationX: Math.PI/2,
        flexRotationX: Math.PI/2,
        oneRotationX: Math.PI/2,
        zipRotationX: Math.PI/2,
        time: 0,
        startAd: false,
    }
});<commit_msg>demo: tighten ad orchestration<commit_after>BEST.component('famous-ads:fitbit', {
    tree: 'fitbit.html',
    behaviors: {
        '#ad-container': {
            'size': function(adSize) {
                return adSize;
            }
        },
        '#container-background': {
            'style': {
                'background-color': 'white',
                'z-index': '-10'
            }
        },
        '#meet-family-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [20, 50]],
                    [750,   [20, 50]],
                    [1000,  [1000, 50], 'outExpo']
                ])(time);
            },
        },
        '#meet-family': {
            'true-size': true,
            'style': {
                'color': 'rgb(71, 187, 179)',
                'font-weight': '800',
                'font-size': '35px',
                'text-align': 'center'
            }
        },
        '#bottom-bar-container': {
            'size': function(bottomBarSize) {
                return bottomBarSize;
            },
            'position': function(bottomBarPosition) {
                return bottomBarPosition;
            }
        },
        '#bottom-bar-background': {
            'style': {
                'background-color': 'rgb(17, 36, 41)',
                'z-index': '-5'
            }
        },
        '#fitbit-logo-container': {
            'position': function(fitbitLogoPosition) {
                return fitbitLogoPosition;
            }
        },
        '#shop-now-container': {
            'size': function(shopNowSize) {
                return shopNowSize;
            },
            'position': function(shopNowPosition) {
                return shopNowPosition;
            }
        },
        '#shop-now': {
            'style': function(shopNowBackgroundColor, shopNowTextColor) {
                return {
                    'background-color': shopNowBackgroundColor,
                    'color': shopNowTextColor,
                    'border-radius': '10px',
                    'text-align': 'center',
                    'font-size': '18px',
                    'transition': 'all 0.25s ease'
                }
            }
        },
        '#charge-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 60]],
                    [750,   [-500, 60]],
                    [1000,  [180,  60], 'easeOut']
                ])(time);
            }
        },
        '#charge-img': {
            'true-size': true
        },
        '#charge-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [750,   [-500, 25]],
                    [1000,  [210,  25], 'easeOut'],
                    [3000,  [210,  25]],
                    [3500,  [1210, 25], 'easeOut'],
                    [3501,  [210,  25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, chargeRotationX) {
                return time > 3501 ? chargeRotationX :
                    _timeline([
                        [0,     0],
                        [3500,  0],
                        [3501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#charge-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500',
            }
        },
        '#flex-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 65]],
                    [900,   [-500, 65]],
                    [1400,  [110,  65], 'easeOut'],
                ])(time);
            },

        },
        '#flex-img': {
            'true-size': true
        },
        '#flex-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [900,   [-500, 25]],
                    [1400,  [140,  25], 'easeOut'],
                    [3000,  [140,  25]],
                    [3500,  [1140, 25], 'easeOut'],
                    [3501,  [140,  25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, flexRotationX) {
                return time > 3501 ? flexRotationX :
                    _timeline([
                        [0,     0],
                        [5500,  0],
                        [5501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#flex-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500',
            }
        },
        '#one-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 82]],
                    [1300,  [-500, 82]],
                    [1800,  [60,   82], 'easeOut']
                ])(time);
            }
        },
        '#one-img': {
            'true-size': true
        },
        '#one-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [1300,  [-500, 25]],
                    [1800,  [70,   25], 'easeOut'],
                    [3000,  [70,   25]],
                    [3500,  [1070, 25], 'easeOut'],
                    [3501,  [70,   25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, oneRotationX) {
                return time > 3501 ? oneRotationX :
                    _timeline([
                        [0,     0],
                        [3500,  0],
                        [3501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#one-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500'
            }
        },
        '#zip-img-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 105]],
                    [1700,  [-500, 105]],
                    [2100,  [0,    105], 'easeOut'],
                ])(time);
            }
        },
        '#zip-img': {
            'true-size': true
        },
        '#zip-label-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-500, 25]],
                    [1700,  [-500, 25]],
                    [2100,  [20,   25], 'easeOut'],
                    [3000,  [20,   25]],
                    [3500,  [1020, 25], 'easeOut'],
                    [3501,  [20,   25]]
                ])(time);
            },
            'rotation-x': function(time, _timeline, zipRotationX) {
                return time > 3501 ? zipRotationX :
                    _timeline([
                        [0,     0],
                        [3500,  0],
                        [3501,  Math.PI/2]
                    ])(time);
            },
            'origin': [1, 1]
        },
        '#zip-label': {
            'true-size': true,
            'style': {
                'font-size': '20px',
                'font-weight': '500'
            }
        },
        '#find-fit-container': {
            'position': function(time, _timeline) {
                return _timeline([
                    [0,     [-1000, 0]],
                    [3000,  [-1000, 0]],
                    [3250,  [75,    10], 'easeOut']
                ])(time);
            },
            'rotation-x': function(findFitRotationX) {
                return findFitRotationX;
            },
            'origin': [0.5, 0.5]
        },
        '#find-fit': {
            'true-size': true,
            'style': {
                'color': 'rgb(71, 187, 179)',
                'font-weight': '800',
                'font-size': '30px',
                'text-align': 'center'
            }
        },
        '$self': {
            '$self:startAd' : function(startAd) {
                return true;
            }
        }
    },
    events: {
        public: {
            'handle-charge-hover': function(state) {
                state
                    .set('chargeRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('flexRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(94, 122, 136)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-flex-hover': function(state) {
                state
                    .set('flexRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('chargeRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(232, 87, 60)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-one-hover': function(state) {
                state
                    .set('oneRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('chargeRotationX', Math.PI/2)
                    .set('flexRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(6, 6, 6)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-zip-hover': function(state) {
                state
                    .set('zipRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('chargeRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('flexRotationX', Math.PI/2)
                    .set('findFitRotationX', Math.PI/2)
                    .set('shopNowBackgroundColor', 'rgb(184, 222, 61)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-exit-hover': function(state) {
                state
                    .set('chargeRotationX', Math.PI/2)
                    .set('flexRotationX', Math.PI/2)
                    .set('oneRotationX', Math.PI/2)
                    .set('zipRotationX', Math.PI/2)
                    .set('findFitRotationX', 0, {duration: 200, curve: 'easeOut'})
                    .set('shopNowBackgroundColor', 'rgb(239, 61, 111)')
                    .set('shopNowTextColor', 'rgb(255, 255, 255)');
            },
            'handle-button-hover': function(state) {
                state
                    .set('shopNowBackgroundColor', 'rgb(255, 255, 255)')
                    .set('shopNowTextColor', 'rgb(239, 61, 111)');
            }
        },
        handlers: {
            'startAd' : function($state, $payload) {
                $state.set('time', 10000, {duration: 10000});
            }
        }
    },
    states: {
        adSize: [300, 250],
        meetFamilyPosition: [20, 50],
        bottomBarSize: [300, 45],
        bottomBarPosition: [0, 205],
        fitbitLogoPosition: [15, 10],
        shopNowSize: [140, 28],
        shopNowPosition: [150, 10],
        shopNowBackgroundColor: 'rgb(239, 61, 111)',
        shopNowTextColor: 'rgb(255, 255, 255)',
        chargeImagePosition: [180, 60],
        chargeLabelPosition: [220, 30],
        flexImagePosition: [110, 65],
        flexLabelPosition: [145, 30],
        oneImagePosition: [60, 82],
        oneLabelPosition: [75, 30],
        zipImagePosition: [0, 105],
        zipLabelPosition: [30, 30],
        findFitPosition: [75, 10],
        findFitRotationX: 0,
        chargeRotationX: Math.PI/2,
        flexRotationX: Math.PI/2,
        oneRotationX: Math.PI/2,
        zipRotationX: Math.PI/2,
        time: 0,
        startAd: false,
    }
});<|endoftext|>


<commit_before>var CatalinSeoHandler = {
    listenersBinded: false,
    isAjaxEnabled: false,
    priceSlider: {
        urlTemplate: '',
        minPrice: 0,
        maxPrice: 0,
        currentMinPrice: 0,
        currentMaxPrice: 0
    },
    handlePriceEvent: function (val) {
        var self = this;
        if (val) {
            var url = self.priceSlider.urlTemplate.replace('__PRICE_VALUE__', val);
            if (self.isAjaxEnabled) {
                self.sendAjaxRequest(url);
            } else {
                window.location.href = url;
            }
        }
    },
    handleEvent: function (el, event) {
        var url;
        var self = this;
        if (el.tagName.toLowerCase() === 'input') {
            url = jQuery(el).val();
        } else if (el.tagName.toLowerCase() === 'a') {
            url = jQuery(el).attr('href');
        } else if (el.tagName.toLowerCase() === 'select') {
            url = jQuery(el).val();
        }

        if (jQuery(el).hasClass('no-ajax')) {
            window.location.href = url;
            return;
        }

        self.sendAjaxRequest(url);

        if (event) {
            event.preventDefault();
        }
    },
    sendAjaxRequest: function(url) {
        var fullUrl;
        var self = this;
        // Add this to query string for full page caching systems
        if (url.indexOf('?') != -1) {
            fullUrl = url + '&isLayerAjax=1';
        } else {
            fullUrl = url + '?isLayerAjax=1';
        }

        $('loading').show();
        $('ajax-errors').hide();

        self.pushState(null, url, false);

        new Ajax.Request(fullUrl, {
            method: 'get',
            onSuccess: function (transport) {
                if (transport.responseJSON) {
                    $('catalog-listing').update(transport.responseJSON.listing);
                    if ($('layered-navigation') != null) {
                    $('layered-navigation').update(transport.responseJSON.layer);
                    };
                    self.pushState({
                        listing: transport.responseJSON.listing,
                        layer: transport.responseJSON.layer
                    }, url, true);
                    self.ajaxListener();
                    self.toggleContent();
                    self.alignProductGridActions();
                    self.blockCollapsing();
                    self.showMoreListener();
                    self.searchBoxListener();

                    if (typeof(ConfigurableSwatchesList) !== 'undefined') {
                        setTimeout(function(){
                            jQuery(document).trigger('product-media-loaded');
                        }, 0);
                    }
                } else {
                    $('ajax-errors').show();
                }
                $('loading').hide();
            },
            onComplete: CatalinSeoHandler.sendUpdateEvent
        });
    },
    sendUpdateEvent: function() {
        jQuery(document).trigger('catalin:updatePage');
    },
    pushState: function (data, link, replace) {
        var History = window.History;
        if (!History.enabled) {
            return false;
        }

        if (replace) {
            History.replaceState(data, document.title, link);
        } else {
            History.pushState(data, document.title, link);
        }
    },
    ajaxListener: function () {
        var self = this;
        var els;
        els = $$('div.pager a').concat(
            $$('div.sorter a'),
            $$('div.pager select'),
            $$('div.sorter select'),
            $$('div.block-layered-nav a'),
            $$('div.block-layered-nav input[type="checkbox"]')
        );
        els.each(function (el) {
            var tagName = el.tagName.toLowerCase();
            if (tagName === 'a') {
                $(el).observe('click', function (event) {
                    self.handleEvent(this, event);
                });
            } else if (tagName === 'select' || tagName === 'input') {
                $(el).setAttribute('onchange', '');
                $(el).observe('change', function (event) {
                    self.handleEvent(this, event);
                });
            }
        });
    },
    bindPriceSlider: function () {
        var self = this;
        new Control.Slider([$('price-min'), $('price-max')], 'price-range', {
                range: $R(self.priceSlider.minPrice, self.priceSlider.maxPrice),
                sliderValue: [self.priceSlider.currentMinPrice, self.priceSlider.currentMaxPrice],
                values: $R(self.priceSlider.minPrice, self.priceSlider.maxPrice),

                restricted: true,
                onChange: function (val) {
                    if (val[0] != self.priceSlider.currentMinPrice || val[1] != self.priceSlider.currentMaxPrice) {
                        $('button-price-slider').value = val.join('-');
                    }
                },
                onSlide: function (val) {
                    $('price-max-display').innerHTML = val[1];
                    $('price-min-display').innerHTML = val[0];
                }
            }
        );
    },
    bindListeners: function () {
        var self = this;
        if (self.listenersBinded || !self.isAjaxEnabled) {
            return false;
        }
        self.listenersBinded = true;
        document.observe("dom:loaded", function () {
            self.ajaxListener();

            (function (History) {
                // Skip empty categories.
                if (!History.enabled || !$('catalog-listing')) {
                    return false;
                }

				if ($('layered-navigation') == null) {
                self.pushState({
                    listing: $('catalog-listing').innerHTML
                }, document.location.href, true);
                } else {
                self.pushState({
                    listing: $('catalog-listing').innerHTML,
                    layer: $('layered-navigation').innerHTML
                }, document.location.href, true);
                };

                // Bind to StateChange Event
                History.Adapter.bind(window, 'popstate', function (event) {
                    if (event.type == 'popstate') {
                        var State = History.getState();
                        $('catalog-listing').update(State.data.listing);
                        $('layered-navigation').update(State.data.layer);
                        self.ajaxListener();
                        self.toggleContent();
                        self.alignProductGridActions();
                        self.blockCollapsing();
                        self.showMoreListener();
                        self.searchBoxListener();

                        if (typeof(ConfigurableSwatchesList) !== 'undefined') {
                            setTimeout(function(){
                                jQuery(document).trigger('product-media-loaded');
                            }, 0);
                        }
                    }
                });
            })(window.History);

            self.showMoreListener();

            self.searchBoxListener();
        });
    },
    toggleContent: function() {
        // ==============================================
        // UI Pattern - Toggle Content (tabs and accordions in one setup)
        // ==============================================

        jQuery('.toggle-content').each(function () {
            var wrapper = jQuery(this);

            var hasTabs = wrapper.hasClass('tabs');
            var hasAccordion = wrapper.hasClass('accordion');
            var startOpen = wrapper.hasClass('open');

            var dl = wrapper.children('dl:first');
            var dts = dl.children('dt');
            var panes = dl.children('dd');
            var groups = new Array(dts, panes);

            //Create a ul for tabs if necessary.
            if (hasTabs) {
                var ul = jQuery('

');
                dts.each(function () {
                    var dt = jQuery(this);
                    var li = jQuery('

');
                    li.html(dt.html());
                    ul.append(li);
                });
                ul.insertBefore(dl);
                var lis = ul.children();
                groups.push(lis);
            }

            //Add "last" classes.
            var i;
            for (i = 0; i < groups.length; i++) {
                groups[i].filter(':last').addClass('last');
            }

            function toggleClasses(clickedItem, group) {
                var index = group.index(clickedItem);
                var i;
                for (i = 0; i < groups.length; i++) {
                    groups[i].removeClass('current');
                    groups[i].eq(index).addClass('current');
                }
            }

            //Toggle on tab (dt) click.
            dts.on('click', function (e) {
                //They clicked the current dt to close it. Restore the wrapper to unclicked state.
                if (jQuery(this).hasClass('current') && wrapper.hasClass('accordion-open')) {
                    wrapper.removeClass('accordion-open');
                } else {
                    //They're clicking something new. Reflect the explicit user interaction.
                    wrapper.addClass('accordion-open');
                }
                toggleClasses(jQuery(this), dts);
            });

            //Toggle on tab (li) click.
            if (hasTabs) {
                lis.on('click', function (e) {
                    toggleClasses(jQuery(this), lis);
                });
                //Open the first tab.
                lis.eq(0).trigger('click');
            }

            //Open the first accordion if desired.
            if (startOpen) {
                dts.eq(0).trigger('click');
            }

        });
    },
    alignProductGridActions: function() {
        // ==============================================
        // Product Listing - Align action buttons/links
        // ==============================================

        // Since the number of columns per grid will vary based on the viewport size, the only way to align the action
        // buttons/links is via JS

        if (jQuery('.products-grid').length) {

            var alignProductGridActions = function () {
                // Loop through each product grid on the page
                jQuery('.products-grid').each(function(){
                    var gridRows = []; // This will store an array per row
                    var tempRow = [];
                    productGridElements = jQuery(this).children('li');
                    productGridElements.each(function (index) {
                        // The JS ought to be agnostic of the specific CSS breakpoints, so we are dynamically checking to find
                        // each row by grouping all cells (eg, li elements) up until we find an element that is cleared.
                        // We are ignoring the first cell since it will always be cleared.
                        if (jQuery(this).css('clear') != 'none' && index != 0) {
                            gridRows.push(tempRow); // Add the previous set of rows to the main array
                            tempRow = []; // Reset the array since we're on a new row
                        }
                        tempRow.push(this);

                        // The last row will not contain any cells that clear that row, so we check to see if this is the last cell
                        // in the grid, and if so, we add its row to the array
                        if (productGridElements.length == index + 1) {
                            gridRows.push(tempRow);
                        }
                    });

                    jQuery.each(gridRows, function () {
                        var tallestProductInfo = 0;
                        jQuery.each(this, function () {
                            // Since this function is called every time the page is resized, we need to remove the min-height
                            // and bottom-padding so each cell can return to its natural size before being measured.
                            jQuery(this).find('.product-info').css({
                                'min-height': '',
                                'padding-bottom': ''
                            });

                            // We are checking the height of .product-info (rather than the entire li), because the images
                            // will not be loaded when this JS is run.
                            var productInfoHeight = jQuery(this).find('.product-info').height();
                            // Space above .actions element
                            var actionSpacing = 10;
                            // The height of the absolutely positioned .actions element
                            var actionHeight = jQuery(this).find('.product-info .actions').height();

                            // Add height of two elements. This is necessary since .actions is absolutely positioned and won't
                            // be included in the height of .product-info
                            var totalHeight = productInfoHeight + actionSpacing + actionHeight;
                            if (totalHeight > tallestProductInfo) {
                                tallestProductInfo = totalHeight;
                            }

                            // Set the bottom-padding to accommodate the height of the .actions element. Note: if .actions
                            // elements are of varying heights, they will not be aligned.
                            jQuery(this).find('.product-info').css('padding-bottom', actionHeight + 'px');
                        });
                        // Set the height of all .product-info elements in a row to the tallest height
                        jQuery.each(this, function () {
                            jQuery(this).find('.product-info').css('min-height', tallestProductInfo);
                        });
                    });
                });
            }
            alignProductGridActions();
        }
    },
    blockCollapsing: function() {
        // ==============================================
        // Block collapsing (on smaller viewports)
        // ==============================================

        if (typeof(enquire) !== 'undefined') {
            enquire.register('(max-width: ' + bp.medium + 'px)', {
                setup: function () {
                    this.toggleElements = jQuery(
                        // This selects the menu on the My Account and CMS pages
                        '.col-left-first .block:not(.block-layered-nav) .block-title, ' +
                        '.col-left-first .block-layered-nav .block-subtitle--filter, ' +
                        '.sidebar:not(.col-left-first) .block .block-title'
                    );
                },
                match: function () {
                    this.toggleElements.toggleSingle();
                },
                unmatch: function () {
                    this.toggleElements.toggleSingle({destruct: true});
                }
            });
        }
    },
    showMoreListener: function() {
        jQuery('div.show_more_filters a').on('click', function (e) {
            jQuery(e.target).parent().parent().parent().find('.filter_hide').toggle();
            jQuery(e.target).parent().parent().parent().parent().prev('.attribute_value_search_box').toggle().find('input').focus();
            if(jQuery(e.target).text() == jQuery(e.target).data('text-more')) {
                jQuery(e.target).text(jQuery(e.target).data('text-less'));
            } else {
                jQuery(e.target).text(jQuery(e.target).data('text-more'));
            }
        });
    },
    searchBoxListener: function() {
        /* Make CSS contains psuedo selector case insensitive */
        jQuery.expr[":"].contains = jQuery.expr.createPseudo(function(arg) {
            return function( elem ) {
                return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
            };
        });

        jQuery('.attribute_value_search_box input').on('keyup', function (e) {
            if(jQuery(e.target).val()) {
                jQuery(e.target).parent().next('dd').find('li').hide();
                jQuery(e.target).parent().next('dd').find('li:contains("' + jQuery(e.target).val() + '")').each(function (i, li) {
                    jQuery(li).show();
                });
            } else {
                jQuery(e.target).parent().next('dd').find('li').show();
            }
        });
    }
}
<commit_msg>Fixed formatting<commit_after>var CatalinSeoHandler = {
    listenersBinded: false,
    isAjaxEnabled: false,
    priceSlider: {
        urlTemplate: '',
        minPrice: 0,
        maxPrice: 0,
        currentMinPrice: 0,
        currentMaxPrice: 0
    },
    handlePriceEvent: function (val) {
        var self = this;
        if (val) {
            var url = self.priceSlider.urlTemplate.replace('__PRICE_VALUE__', val);
            if (self.isAjaxEnabled) {
                self.sendAjaxRequest(url);
            } else {
                window.location.href = url;
            }
        }
    },
    handleEvent: function (el, event) {
        var url;
        var self = this;
        if (el.tagName.toLowerCase() === 'input') {
            url = jQuery(el).val();
        } else if (el.tagName.toLowerCase() === 'a') {
            url = jQuery(el).attr('href');
        } else if (el.tagName.toLowerCase() === 'select') {
            url = jQuery(el).val();
        }

        if (jQuery(el).hasClass('no-ajax')) {
            window.location.href = url;
            return;
        }

        self.sendAjaxRequest(url);

        if (event) {
            event.preventDefault();
        }
    },
    sendAjaxRequest: function(url) {
        var fullUrl;
        var self = this;
        // Add this to query string for full page caching systems
        if (url.indexOf('?') != -1) {
            fullUrl = url + '&isLayerAjax=1';
        } else {
            fullUrl = url + '?isLayerAjax=1';
        }

        $('loading').show();
        $('ajax-errors').hide();

        self.pushState(null, url, false);

        new Ajax.Request(fullUrl, {
            method: 'get',
            onSuccess: function (transport) {
                if (transport.responseJSON) {
                    $('catalog-listing').update(transport.responseJSON.listing);
                    if ($('layered-navigation') != null) {
                        $('layered-navigation').update(transport.responseJSON.layer);
                    };
                    self.pushState({
                        listing: transport.responseJSON.listing,
                        layer: transport.responseJSON.layer
                    }, url, true);
                    self.ajaxListener();
                    self.toggleContent();
                    self.alignProductGridActions();
                    self.blockCollapsing();
                    self.showMoreListener();
                    self.searchBoxListener();

                    if (typeof(ConfigurableSwatchesList) !== 'undefined') {
                        setTimeout(function(){
                            jQuery(document).trigger('product-media-loaded');
                        }, 0);
                    }
                } else {
                    $('ajax-errors').show();
                }
                $('loading').hide();
            },
            onComplete: CatalinSeoHandler.sendUpdateEvent
        });
    },
    sendUpdateEvent: function() {
        jQuery(document).trigger('catalin:updatePage');
    },
    pushState: function (data, link, replace) {
        var History = window.History;
        if (!History.enabled) {
            return false;
        }

        if (replace) {
            History.replaceState(data, document.title, link);
        } else {
            History.pushState(data, document.title, link);
        }
    },
    ajaxListener: function () {
        var self = this;
        var els;
        els = $$('div.pager a').concat(
            $$('div.sorter a'),
            $$('div.pager select'),
            $$('div.sorter select'),
            $$('div.block-layered-nav a'),
            $$('div.block-layered-nav input[type="checkbox"]')
        );
        els.each(function (el) {
            var tagName = el.tagName.toLowerCase();
            if (tagName === 'a') {
                $(el).observe('click', function (event) {
                    self.handleEvent(this, event);
                });
            } else if (tagName === 'select' || tagName === 'input') {
                $(el).setAttribute('onchange', '');
                $(el).observe('change', function (event) {
                    self.handleEvent(this, event);
                });
            }
        });
    },
    bindPriceSlider: function () {
        var self = this;
        new Control.Slider([$('price-min'), $('price-max')], 'price-range', {
                range: $R(self.priceSlider.minPrice, self.priceSlider.maxPrice),
                sliderValue: [self.priceSlider.currentMinPrice, self.priceSlider.currentMaxPrice],
                values: $R(self.priceSlider.minPrice, self.priceSlider.maxPrice),

                restricted: true,
                onChange: function (val) {
                    if (val[0] != self.priceSlider.currentMinPrice || val[1] != self.priceSlider.currentMaxPrice) {
                        $('button-price-slider').value = val.join('-');
                    }
                },
                onSlide: function (val) {
                    $('price-max-display').innerHTML = val[1];
                    $('price-min-display').innerHTML = val[0];
                }
            }
        );
    },
    bindListeners: function () {
        var self = this;
        if (self.listenersBinded || !self.isAjaxEnabled) {
            return false;
        }
        self.listenersBinded = true;
        document.observe("dom:loaded", function () {
            self.ajaxListener();

            (function (History) {
                // Skip empty categories.
                if (!History.enabled || !$('catalog-listing')) {
                    return false;
                }

                if ($('layered-navigation') == null) {
                    self.pushState({
                        listing: $('catalog-listing').innerHTML
                    }, document.location.href, true);
                } else {
                    self.pushState({
                        listing: $('catalog-listing').innerHTML,
                        layer: $('layered-navigation').innerHTML
                    }, document.location.href, true);
                };

                // Bind to StateChange Event
                History.Adapter.bind(window, 'popstate', function (event) {
                    if (event.type == 'popstate') {
                        var State = History.getState();
                        $('catalog-listing').update(State.data.listing);
                        $('layered-navigation').update(State.data.layer);
                        self.ajaxListener();
                        self.toggleContent();
                        self.alignProductGridActions();
                        self.blockCollapsing();
                        self.showMoreListener();
                        self.searchBoxListener();

                        if (typeof(ConfigurableSwatchesList) !== 'undefined') {
                            setTimeout(function(){
                                jQuery(document).trigger('product-media-loaded');
                            }, 0);
                        }
                    }
                });
            })(window.History);

            self.showMoreListener();

            self.searchBoxListener();
        });
    },
    toggleContent: function() {
        // ==============================================
        // UI Pattern - Toggle Content (tabs and accordions in one setup)
        // ==============================================

        jQuery('.toggle-content').each(function () {
            var wrapper = jQuery(this);

            var hasTabs = wrapper.hasClass('tabs');
            var hasAccordion = wrapper.hasClass('accordion');
            var startOpen = wrapper.hasClass('open');

            var dl = wrapper.children('dl:first');
            var dts = dl.children('dt');
            var panes = dl.children('dd');
            var groups = new Array(dts, panes);

            //Create a ul for tabs if necessary.
            if (hasTabs) {
                var ul = jQuery('

');
                dts.each(function () {
                    var dt = jQuery(this);
                    var li = jQuery('

');
                    li.html(dt.html());
                    ul.append(li);
                });
                ul.insertBefore(dl);
                var lis = ul.children();
                groups.push(lis);
            }

            //Add "last" classes.
            var i;
            for (i = 0; i < groups.length; i++) {
                groups[i].filter(':last').addClass('last');
            }

            function toggleClasses(clickedItem, group) {
                var index = group.index(clickedItem);
                var i;
                for (i = 0; i < groups.length; i++) {
                    groups[i].removeClass('current');
                    groups[i].eq(index).addClass('current');
                }
            }

            //Toggle on tab (dt) click.
            dts.on('click', function (e) {
                //They clicked the current dt to close it. Restore the wrapper to unclicked state.
                if (jQuery(this).hasClass('current') && wrapper.hasClass('accordion-open')) {
                    wrapper.removeClass('accordion-open');
                } else {
                    //They're clicking something new. Reflect the explicit user interaction.
                    wrapper.addClass('accordion-open');
                }
                toggleClasses(jQuery(this), dts);
            });

            //Toggle on tab (li) click.
            if (hasTabs) {
                lis.on('click', function (e) {
                    toggleClasses(jQuery(this), lis);
                });
                //Open the first tab.
                lis.eq(0).trigger('click');
            }

            //Open the first accordion if desired.
            if (startOpen) {
                dts.eq(0).trigger('click');
            }

        });
    },
    alignProductGridActions: function() {
        // ==============================================
        // Product Listing - Align action buttons/links
        // ==============================================

        // Since the number of columns per grid will vary based on the viewport size, the only way to align the action
        // buttons/links is via JS

        if (jQuery('.products-grid').length) {

            var alignProductGridActions = function () {
                // Loop through each product grid on the page
                jQuery('.products-grid').each(function(){
                    var gridRows = []; // This will store an array per row
                    var tempRow = [];
                    productGridElements = jQuery(this).children('li');
                    productGridElements.each(function (index) {
                        // The JS ought to be agnostic of the specific CSS breakpoints, so we are dynamically checking to find
                        // each row by grouping all cells (eg, li elements) up until we find an element that is cleared.
                        // We are ignoring the first cell since it will always be cleared.
                        if (jQuery(this).css('clear') != 'none' && index != 0) {
                            gridRows.push(tempRow); // Add the previous set of rows to the main array
                            tempRow = []; // Reset the array since we're on a new row
                        }
                        tempRow.push(this);

                        // The last row will not contain any cells that clear that row, so we check to see if this is the last cell
                        // in the grid, and if so, we add its row to the array
                        if (productGridElements.length == index + 1) {
                            gridRows.push(tempRow);
                        }
                    });

                    jQuery.each(gridRows, function () {
                        var tallestProductInfo = 0;
                        jQuery.each(this, function () {
                            // Since this function is called every time the page is resized, we need to remove the min-height
                            // and bottom-padding so each cell can return to its natural size before being measured.
                            jQuery(this).find('.product-info').css({
                                'min-height': '',
                                'padding-bottom': ''
                            });

                            // We are checking the height of .product-info (rather than the entire li), because the images
                            // will not be loaded when this JS is run.
                            var productInfoHeight = jQuery(this).find('.product-info').height();
                            // Space above .actions element
                            var actionSpacing = 10;
                            // The height of the absolutely positioned .actions element
                            var actionHeight = jQuery(this).find('.product-info .actions').height();

                            // Add height of two elements. This is necessary since .actions is absolutely positioned and won't
                            // be included in the height of .product-info
                            var totalHeight = productInfoHeight + actionSpacing + actionHeight;
                            if (totalHeight > tallestProductInfo) {
                                tallestProductInfo = totalHeight;
                            }

                            // Set the bottom-padding to accommodate the height of the .actions element. Note: if .actions
                            // elements are of varying heights, they will not be aligned.
                            jQuery(this).find('.product-info').css('padding-bottom', actionHeight + 'px');
                        });
                        // Set the height of all .product-info elements in a row to the tallest height
                        jQuery.each(this, function () {
                            jQuery(this).find('.product-info').css('min-height', tallestProductInfo);
                        });
                    });
                });
            }
            alignProductGridActions();
        }
    },
    blockCollapsing: function() {
        // ==============================================
        // Block collapsing (on smaller viewports)
        // ==============================================

        if (typeof(enquire) !== 'undefined') {
            enquire.register('(max-width: ' + bp.medium + 'px)', {
                setup: function () {
                    this.toggleElements = jQuery(
                        // This selects the menu on the My Account and CMS pages
                        '.col-left-first .block:not(.block-layered-nav) .block-title, ' +
                        '.col-left-first .block-layered-nav .block-subtitle--filter, ' +
                        '.sidebar:not(.col-left-first) .block .block-title'
                    );
                },
                match: function () {
                    this.toggleElements.toggleSingle();
                },
                unmatch: function () {
                    this.toggleElements.toggleSingle({destruct: true});
                }
            });
        }
    },
    showMoreListener: function() {
        jQuery('div.show_more_filters a').on('click', function (e) {
            jQuery(e.target).parent().parent().parent().find('.filter_hide').toggle();
            jQuery(e.target).parent().parent().parent().parent().prev('.attribute_value_search_box').toggle().find('input').focus();
            if(jQuery(e.target).text() == jQuery(e.target).data('text-more')) {
                jQuery(e.target).text(jQuery(e.target).data('text-less'));
            } else {
                jQuery(e.target).text(jQuery(e.target).data('text-more'));
            }
        });
    },
    searchBoxListener: function() {
        /* Make CSS contains psuedo selector case insensitive */
        jQuery.expr[":"].contains = jQuery.expr.createPseudo(function(arg) {
            return function( elem ) {
                return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
            };
        });

        jQuery('.attribute_value_search_box input').on('keyup', function (e) {
            if(jQuery(e.target).val()) {
                jQuery(e.target).parent().next('dd').find('li').hide();
                jQuery(e.target).parent().next('dd').find('li:contains("' + jQuery(e.target).val() + '")').each(function (i, li) {
                    jQuery(li).show();
                });
            } else {
                jQuery(e.target).parent().next('dd').find('li').show();
            }
        });
    }
}
<|endoftext|>


<commit_before>const {readFileSync} = require('fs');
const normalize = require('../utils/keymaps/normalize');
const {defaultPlatformKeyPath} = require('./paths');

const commands = {};
const keys = {};

const _setKeysForCommands = function (keymap) {
  for (const command in keymap) {
    if (command) {
      commands[command] = normalize(keymap[command]);
    }
  }
};

const _setCommandsForKeys = function (commands) {
  for (const command in commands) {
    if (command) {
      keys[commands[command]] = command;
    }
  }
};

const _import = function (customsKeys) {
  try {
    const mapping = JSON.parse(readFileSync(defaultPlatformKeyPath()));
    _setKeysForCommands(mapping);
    _setKeysForCommands(customsKeys);
    _setCommandsForKeys(commands);

    return {commands, keys};
  } catch (err) {}
};

const _extend = function (customsKeys) {
  if (customsKeys) {
    for (const command in customsKeys) {
      if (command) {
        commands[command] = normalize(customsKeys[command]);
        keys[normalize(customsKeys[command])] = command;
      }
    }
  }
  return {commands, keys};
};

module.exports = {
  import: _import,
  extend: _extend
};
<commit_msg>Fix typo in utils customKeys (#2283)<commit_after>const {readFileSync} = require('fs');
const normalize = require('../utils/keymaps/normalize');
const {defaultPlatformKeyPath} = require('./paths');

const commands = {};
const keys = {};

const _setKeysForCommands = function (keymap) {
  for (const command in keymap) {
    if (command) {
      commands[command] = normalize(keymap[command]);
    }
  }
};

const _setCommandsForKeys = function (commands) {
  for (const command in commands) {
    if (command) {
      keys[commands[command]] = command;
    }
  }
};

const _import = function (customKeys) {
  try {
    const mapping = JSON.parse(readFileSync(defaultPlatformKeyPath()));
    _setKeysForCommands(mapping);
    _setKeysForCommands(customKeys);
    _setCommandsForKeys(commands);

    return {commands, keys};
  } catch (err) {}
};

const _extend = function (customKeys) {
  if (customKeys) {
    for (const command in customKeys) {
      if (command) {
        commands[command] = normalize(customKeys[command]);
        keys[normalize(customKeys[command])] = command;
      }
    }
  }
  return {commands, keys};
};

module.exports = {
  import: _import,
  extend: _extend
};
<|endoftext|>


<commit_before>'use strict';

describe('mapService', function(){
    var mapService, $rootScope, $scope, leafletData, deferred, $q, $httpBackend,
        $templateCache, shareUtilService;

    beforeEach(module('owh'));

    beforeEach(inject(function ($injector, _$rootScope_, _$q_, _$templateCache_) {
        mapService = $injector.get('mapService');
        leafletData = $injector.get('leafletData');
        shareUtilService = $injector.get('shareUtilService');
        $rootScope  = _$rootScope_;
        $scope = $rootScope.$new();
        $q = _$q_;
        deferred = $q.defer();
        $httpBackend = $injector.get('$httpBackend');
        $templateCache = _$templateCache_;
        mapService.shareUtilService = shareUtilService;

        $templateCache.put('app/partials/marker-template.html', 'app/partials/marker-template.html');
        $templateCache.put('app/modules/home/home.html', 'app/modules/home/home.html');
        $httpBackend.whenGET('app/i18n/messages-en.json').respond({ hello: 'World' });
        $httpBackend.whenGET('app/partials/marker-template.html').respond( $templateCache.get('app/partials/marker-template.html'));
        $httpBackend.whenGET('/getFBAppID').respond({data: { fbAppID: 1111111111111111}});
        $httpBackend.whenGET('/yrbsQuestionsTree').respond({data: { }});
        $httpBackend.whenGET('/pramsQuestionsTree').respond({data: { }});
    }));

    describe('test map custom controls', function() {
        it('should add custom legend', function () {
            var mapData = {mapMinValue:40289400, mapMaxValue:58000};
            var control = mapService.addScaleControl(mapData);
            //test callback
            var mapControl = new control();
            expect(mapControl.options.position).toEqual('bottomleft');
            mapControl.onAdd();
        });

        it('should trigger mouseover & mouseout event', function () {
            var mapData = {mapMinValue:40289400, mapMaxValue:58000};
            var control = mapService.addScaleControl(mapData);
            var mapControl = new control();

            var map = {_layers:[{options:{fillColor:'#190032'}, style:{color: 'black', fillOpacity: 0.7},
                setStyle:function (style) {this.style = style }},
                {options:{fillColor:'#3f007d'}, style:{color: 'black', fillOpacity: 0.7},
                    setStyle:function (style) { this.style = style }}]};

            var legend = mapControl.onAdd(map);

            //mouseover event
            var evt = document.createEvent('MouseEvents');
            evt.initEvent('mouseover', true, true);
            var legendInterval = legend.childNodes[0].childNodes[0].childNodes[0];
            legendInterval.dispatchEvent(evt);

            //style changed
            expect(map._layers[0].style).toEqual({"color":"#333333","weight":2.6,"opacity":1,"fillOpacity":0.9});
            //no style change
            expect(map._layers[1].style).toEqual({color: 'black', fillOpacity: 0.7});

            //mouseout event
            evt = document.createEvent('MouseEvents');
            evt.initEvent('mouseout', true, true);
            legendInterval.dispatchEvent(evt);
            //reset style
            expect(map._layers[0].style).toEqual({weight: 0.8, opacity: 1, color: 'black', fillOpacity: 0.7});

        });

        it('should add share control', function () {
            var mapData = {mapMinValue:40289400, mapMaxValue:58000};
            var control = mapService.addShareControl(mapData);
            var map = {zoomIn:function(){}, zoomOut:function(){},
                getSize:function(){return 12}, eachLayer:function(){}};

            var mapControl = new control();
            expect(mapControl.options.position).toEqual('topright');

            var shareControl = mapControl.onAdd();
            expect(shareControl.title).toEqual('label.share.on.fb');

            spyOn(leafletData, 'getMap').and.returnValue(deferred.promise);
            spyOn(shareUtilService, 'shareOnFb');

            shareControl.onclick();

            deferred.resolve(map);
            $scope.$apply();

        });

        it('should expand map', function () {
            var mapData = {mapData : {mapMinValue:40289400, mapMaxValue:58000}};
            //previous size
            var mapSize = {selectedMapSize:'small'};
            var map = {zoomIn:function(){}, zoomOut:function(){}};

            spyOn(leafletData, 'getMap').and.returnValue(deferred.promise);

            var control = mapService.addExpandControl(mapSize, mapData);
            var mapControl = new control();

            expect(mapControl.options.position).toEqual('topright');
            var expandControl = mapControl.onAdd();
            expandControl.onclick();
            deferred.resolve(map);
            $scope.$apply();
            //new size
            expect(mapSize.selectedMapSize).toEqual('big');
        });

        it('should collapse map', function () {
            var mapData = {mapData : {mapMinValue:40289400, mapMaxValue:58000}};
            //previous size
            var mapSize = {selectedMapSize:'big'};
            var map = {zoomIn:function(){}, zoomOut:function(){}};

            spyOn(leafletData, 'getMap').and.returnValue(deferred.promise);

            var control = mapService.addExpandControl(mapSize, mapData);
            var mapControl = new control();

            expect(mapControl.options.position).toEqual('topright');
            var expandControl = mapControl.onAdd();
            expandControl.onclick();
            deferred.resolve(map);
            $scope.$apply();
            //new size
            expect(mapSize.selectedMapSize).toEqual('small');
        });
    });
});
<commit_msg>owh-1091 added comment<commit_after>'use strict';

describe('mapService', function(){
    var mapService, $rootScope, $scope, leafletData, deferred, $q, $httpBackend,
        $templateCache, shareUtilService;

    beforeEach(module('owh'));

    beforeEach(inject(function ($injector, _$rootScope_, _$q_, _$templateCache_) {
        mapService = $injector.get('mapService');
        leafletData = $injector.get('leafletData');
        shareUtilService = $injector.get('shareUtilService');
        $rootScope  = _$rootScope_;
        $scope = $rootScope.$new();
        $q = _$q_;
        deferred = $q.defer();
        $httpBackend = $injector.get('$httpBackend');
        $templateCache = _$templateCache_;
        mapService.shareUtilService = shareUtilService;

        $templateCache.put('app/partials/marker-template.html', 'app/partials/marker-template.html');
        $templateCache.put('app/modules/home/home.html', 'app/modules/home/home.html');
        $httpBackend.whenGET('app/i18n/messages-en.json').respond({ hello: 'World' });
        $httpBackend.whenGET('app/partials/marker-template.html').respond( $templateCache.get('app/partials/marker-template.html'));
        $httpBackend.whenGET('/getFBAppID').respond({data: { fbAppID: 1111111111111111}});
        $httpBackend.whenGET('/yrbsQuestionsTree').respond({data: { }});
        $httpBackend.whenGET('/pramsQuestionsTree').respond({data: { }});
    }));

    describe('test map custom controls', function() {
        it('should add custom legend', function () {
            var mapData = {mapMinValue:40289400, mapMaxValue:58000};
            var control = mapService.addScaleControl(mapData);
            //test callback
            var mapControl = new control();
            expect(mapControl.options.position).toEqual('bottomleft');
            mapControl.onAdd();
        });

        it('should trigger mouseover & mouseout event', function () {
            var mapData = {mapMinValue:40289400, mapMaxValue:58000};
            var control = mapService.addScaleControl(mapData);
            var mapControl = new control();

            var map = {_layers:[{options:{fillColor:'#190032'}, style:{color: 'black', fillOpacity: 0.7},
                setStyle:function (style) {this.style = style }},
                {options:{fillColor:'#3f007d'}, style:{color: 'black', fillOpacity: 0.7},
                    setStyle:function (style) { this.style = style }}]};

            var legend = mapControl.onAdd(map);

            //mouseover event
            var evt = document.createEvent('MouseEvents');
            evt.initEvent('mouseover', true, true);
            var legendInterval = legend.childNodes[0].childNodes[0].childNodes[0];
            legendInterval.dispatchEvent(evt);

            //style changed
            expect(map._layers[0].style).toEqual({"color":"#333333","weight":2.6,"opacity":1,"fillOpacity":0.9});
            //no style change for polygon which does not match event target
            expect(map._layers[1].style).toEqual({color: 'black', fillOpacity: 0.7});

            //mouseout event
            evt = document.createEvent('MouseEvents');
            evt.initEvent('mouseout', true, true);
            legendInterval.dispatchEvent(evt);
            //reset style
            expect(map._layers[0].style).toEqual({weight: 0.8, opacity: 1, color: 'black', fillOpacity: 0.7});

        });

        it('should add share control', function () {
            var mapData = {mapMinValue:40289400, mapMaxValue:58000};
            var control = mapService.addShareControl(mapData);
            var map = {zoomIn:function(){}, zoomOut:function(){},
                getSize:function(){return 12}, eachLayer:function(){}};

            var mapControl = new control();
            expect(mapControl.options.position).toEqual('topright');

            var shareControl = mapControl.onAdd();
            expect(shareControl.title).toEqual('label.share.on.fb');

            spyOn(leafletData, 'getMap').and.returnValue(deferred.promise);
            spyOn(shareUtilService, 'shareOnFb');

            shareControl.onclick();

            deferred.resolve(map);
            $scope.$apply();

        });

        it('should expand map', function () {
            var mapData = {mapData : {mapMinValue:40289400, mapMaxValue:58000}};
            //previous size
            var mapSize = {selectedMapSize:'small'};
            var map = {zoomIn:function(){}, zoomOut:function(){}};

            spyOn(leafletData, 'getMap').and.returnValue(deferred.promise);

            var control = mapService.addExpandControl(mapSize, mapData);
            var mapControl = new control();

            expect(mapControl.options.position).toEqual('topright');
            var expandControl = mapControl.onAdd();
            expandControl.onclick();
            deferred.resolve(map);
            $scope.$apply();
            //new size
            expect(mapSize.selectedMapSize).toEqual('big');
        });

        it('should collapse map', function () {
            var mapData = {mapData : {mapMinValue:40289400, mapMaxValue:58000}};
            //previous size
            var mapSize = {selectedMapSize:'big'};
            var map = {zoomIn:function(){}, zoomOut:function(){}};

            spyOn(leafletData, 'getMap').and.returnValue(deferred.promise);

            var control = mapService.addExpandControl(mapSize, mapData);
            var mapControl = new control();

            expect(mapControl.options.position).toEqual('topright');
            var expandControl = mapControl.onAdd();
            expandControl.onclick();
            deferred.resolve(map);
            $scope.$apply();
            //new size
            expect(mapSize.selectedMapSize).toEqual('small');
        });
    });
});
<|endoftext|>


<commit_before>
export function attr(name, value) {

  if (arguments.length < 2) {
    for (value in name) {
      this.each(selectionAttr(value, name[value]));
    }
    return this;
  }
  return this.each(selectionAttr(name, value));
}

function selectionAttr(name, value) {

  function attrNull() {
    delete this[name];
  }

  function attrConstant() {
    if (name === "tags" || name === "class") {
      var arr = value.split(" ");
      for (var i = 0; i < arr.length; i++) {
        this.__tags__.push(arr[i]);
      }
    } else {
      this[name] = value;
    }
  }

  function attrFunction() {
    var x = value.apply(this, arguments);
    if (x === null) {
      return this[name] && delete this[name];
    } else {
      this[name] = x;
    }
  }
  return value === null ? attrNull: (typeof value === "function" ? attrFunction: attrConstant);
}
<commit_msg>handle scale, position, and rotation attributes<commit_after>
export function attr(name, value) {

  if (arguments.length < 2) {
    for (value in name) {
      this.each(selectionAttr(value, name[value]));
    }
    return this;
  }
  return this.each(selectionAttr(name, value));
}

function selectionAttr(name, value) {

  function attrNull() {
    delete this[name];
  }

  function attrConstant() {
    if (name === "tags" || name === "class") {
      var arr = value.split(" ");
      for (var i = 0; i < arr.length; i++) {
        this.__tags__.push(arr[i]);
      }
    } else if (name === "position" || name === "rotation" || name === "scale") {
      this[name].copy(value);
    } else {
      this[name] = value;
    }
  }

  function attrFunction() {
    var x = value.apply(this, arguments);
    if (x === null) {
      return this[name] && delete this[name];
    } else if (name === "position" || name === "rotation" || name === "scale") {
      this[name].copy(x);
    } else {
      this[name] = x;
    }
  }
  return value === null ? attrNull: (typeof value === "function" ? attrFunction: attrConstant);
}
<|endoftext|>


<commit_before>require('proof')(1, require('cadence')(prove))

require('compassion.test')

function prove (async, assert) {
    var supervisor = require('../compassion.bin')

    var io
    async(function () {
        io = supervisor([ 'test', 'a' ], {}, async())
    }, function (code) {
        assert(code, 0, 'code')
    })
}
<commit_msg>Rename `assert` to `okay`.<commit_after>require('proof')(1, require('cadence')(prove))

require('compassion.test')

function prove (async, okay) {
    var supervisor = require('../compassion.bin')

    var io
    async(function () {
        io = supervisor([ 'test', 'a' ], {}, async())
    }, function (code) {
        okay(code, 0, 'code')
    })
}
<|endoftext|>


<commit_before>import React from 'react'
import { Router, RoutingContext, match } from 'react-router'
import createLocation from 'history/lib/createLocation'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reduxPromise from 'shared/utils/promiseMiddleware'
import * as reducers from 'shared/reducers'
import { Provider } from 'react-redux'
import routes from 'shared/routes'
import fs from 'fs'
import path from 'path'
import nunjucks from 'nunjucks'
import DocumentTitle from 'shared/components/addon/document-title'
import * as AuthActions from 'shared/actions/AuthActions'
import * as LocaleActions from 'shared/actions/LocaleActions'
import url from 'url'
import AppContainer from 'shared/components/AppContainer'
import route from 'koa-route'
import ReduxUniversalResolver from 'shared/utils/redux-universal-resolver'
import { originLocaleName } from 'shared/utils/locale-utils'

const Translator = require('counterpart').Instance

nunjucks.configure('views', {
  autoescape: true
})

// thunkify stat
function stat (file) {
  return function (done) {
    fs.stat(file, done)
  }
}

export default function (app) {
  app.use(route.get('/uploads/*', function *() {
    const location = path.join(__dirname, '../../', decodeURI(this.path))
    const fstat = yield stat(location)

    if (fstat.isFile()) {
      this.type = path.extname(location)
      this.body = fs.createReadStream(location)
    }
  }))

  app.use(function *() {
    const isCashed = this.cashed ? yield *this.cashed() : false
    if (!isCashed) {
      const reducer = combineReducers(reducers)
      const finalCreateStore = applyMiddleware(
        thunk,
        reduxPromise
      )(createStore)
      const store = finalCreateStore(reducer)
      const resolver = new ReduxUniversalResolver()
      store.resolver = resolver

      const location = createLocation(this.url)
      // save session token to store
      if (this.session.token && this.session.token !== null) {
        store.dispatch(AuthActions.sync(this.session.token))
      }
      /* eslint-disable max-len */
      const translator = new Translator()
      translator.registerTranslations('en', require('shared/i18n/en'))
      translator.registerTranslations('zh-hant-tw', require('shared/i18n/zh-hant-tw'))
      translator.registerTranslations('zh-hant-cn', require('shared/i18n/zh-hant-cn'))
      translator.setFallbackLocale('zh-hant-tw')

      let lang = originLocaleName(this.getLocaleFromHeader())
      store.dispatch(LocaleActions.sync(lang || 'zh-hant-tw'))
      translator.setLocale(lang || 'zh-hant-tw')

      let appString
      let assets
      let title
      let siteUrl
      let ogImage
      const { error, redirectLocation, renderProps }
      = yield new Promise((resolve) => {
        match({routes: routes(store), location},
          (_error, _redirectLocation, _renderProps) => {
            resolve({
              error: _error,
              _redirectLocation: _redirectLocation,
              renderProps: _renderProps
            })
          })
      })

      if (redirectLocation) {
        this.redirect(redirectLocation.pathname + redirectLocation.search)
        return
      } else if (error) {
        this.redirect('/')
        return
      } else if (renderProps === null) {
        this.redirect('/')
        return
      }

      const elements = (
        <AppContainer translator={translator}>
          {() =>
            <Provider store={store}>
              {() =>
                <RoutingContext {...renderProps} />
              }
            </Provider>
          }
        </AppContainer>
      )

      React.renderToString(elements)  // need this line to collect data
      yield resolver.dispatch()
      appString = React.renderToString(elements)

      title = DocumentTitle.rewind()

      const env = process.env.NODE_ENV
      if (env === 'development') {
        assets = fs.readFileSync(
          path.resolve(__dirname, '../../storage/webpack-stats.json')
        )
        assets = JSON.parse(assets)
      } else {
        assets = require('storage/webpack-stats.json')
      }

      const serverState = React.renderToString(
        <script
          dangerouslySetInnerHTML=
            {{
              __html:
                `window.STATE_FROM_SERVER=${JSON.stringify(store.getState())};`
            }} />
      )

      siteUrl = this.href
      ogImage = this.protocol + '://' + this.host + '/images/icon.png'
      this.body = nunjucks.render('index.html', {
        appString,
        assets,
        siteUrl,
        ogImage,
        env: process.env,
        title,
        stateFromServer: serverState
      })

      resolver.clear()
    }
  })
}
<commit_msg>cleanup<commit_after>import React from 'react'
import { Router, RoutingContext, match } from 'react-router'
import createLocation from 'history/lib/createLocation'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reduxPromise from 'shared/utils/promiseMiddleware'
import * as reducers from 'shared/reducers'
import { Provider } from 'react-redux'
import routes from 'shared/routes'
import fs from 'fs'
import path from 'path'
import nunjucks from 'nunjucks'
import DocumentTitle from 'shared/components/addon/document-title'
import * as AuthActions from 'shared/actions/AuthActions'
import * as LocaleActions from 'shared/actions/LocaleActions'
import url from 'url'
import AppContainer from 'shared/components/AppContainer'
import route from 'koa-route'
import ReduxUniversalResolver from 'shared/utils/redux-universal-resolver'
import { originLocaleName } from 'shared/utils/locale-utils'

const Translator = require('counterpart').Instance

nunjucks.configure('views', {
  autoescape: true
})

// thunkify stat
function stat (file) {
  return function (done) {
    fs.stat(file, done)
  }
}

export default function (app) {
  app.use(route.get('/uploads/*', function *() {
    const location = path.join(__dirname, '../../', decodeURI(this.path))
    const fstat = yield stat(location)

    if (fstat.isFile()) {
      this.type = path.extname(location)
      this.body = fs.createReadStream(location)
    }
  }))

  app.use(function *() {
    const isCashed = this.cashed ? yield *this.cashed() : false
    if (!isCashed) {
      const reducer = combineReducers(reducers)
      const finalCreateStore = applyMiddleware(
        thunk,
        reduxPromise
      )(createStore)
      const store = finalCreateStore(reducer)
      const resolver = new ReduxUniversalResolver()
      store.resolver = resolver

      const location = createLocation(this.url)
      // save session token to store
      if (this.session.token && this.session.token !== null) {
        store.dispatch(AuthActions.sync(this.session.token))
      }
      /* eslint-disable max-len */
      const translator = new Translator()
      translator.registerTranslations('en', require('shared/i18n/en'))
      translator.registerTranslations('zh-hant-tw', require('shared/i18n/zh-hant-tw'))
      translator.registerTranslations('zh-hant-cn', require('shared/i18n/zh-hant-cn'))
      translator.setFallbackLocale('zh-hant-tw')

      const lang = originLocaleName(this.getLocaleFromHeader())
      store.dispatch(LocaleActions.sync(lang || 'zh-hant-tw'))
      translator.setLocale(lang || 'zh-hant-tw')

      let appString
      let assets
      let title
      let siteUrl
      let ogImage
      const { error, redirectLocation, renderProps }
      = yield new Promise((resolve) => {
        match({routes: routes(store), location},
          (_error, _redirectLocation, _renderProps) => {
            resolve({
              error: _error,
              _redirectLocation: _redirectLocation,
              renderProps: _renderProps
            })
          })
      })

      if (redirectLocation) {
        this.redirect(redirectLocation.pathname + redirectLocation.search)
        return
      } else if (error) {
        this.redirect('/')
        return
      } else if (renderProps === null) {
        this.redirect('/')
        return
      }

      const elements = (
        <AppContainer translator={translator}>
          {() =>
            <Provider store={store}>
              {() =>
                <RoutingContext {...renderProps} />
              }
            </Provider>
          }
        </AppContainer>
      )

      React.renderToString(elements)  // need this line to collect data
      yield resolver.dispatch()
      appString = React.renderToString(elements)

      title = DocumentTitle.rewind()

      const env = process.env.NODE_ENV
      if (env === 'development') {
        assets = fs.readFileSync(
          path.resolve(__dirname, '../../storage/webpack-stats.json')
        )
        assets = JSON.parse(assets)
      } else {
        assets = require('storage/webpack-stats.json')
      }

      const serverState = React.renderToString(
        <script
          dangerouslySetInnerHTML=
            {{
              __html:
                `window.STATE_FROM_SERVER=${JSON.stringify(store.getState())};`
            }} />
      )

      siteUrl = this.href
      ogImage = this.protocol + '://' + this.host + '/images/icon.png'
      this.body = nunjucks.render('index.html', {
        appString,
        assets,
        siteUrl,
        ogImage,
        env: process.env,
        title,
        stateFromServer: serverState
      })

      resolver.clear()
    }
  })
}
<|endoftext|>


<commit_before>"use strict";

var express = require('express');

/*
    Title: restapi

    This module creates an expressjs webapp that calls out to the given <GameController> and sends the results back as JSON.  For example, the REST API can be installed within a parent webapp at the sub-URL '/api' by doing:

    | var gc = createMyGameController(...);
    | var app = require('express').createServer();
    | app.use('/api', require('restapi')(gc));

    If the current request's session object has a `playerID' key, the <PlayerState> for that player will be loaded and used as the context.  Alternatively, the parent app can populate the `playerState' property of the request object to get the same effect.

    In general, all data objects are turned into JSON by calling a `toJSON' method.

    * Specs: <http://fluencychallenge.com/wiki/DesignAndImplementation/GameController#RESTAPI>
*/

module.exports = function restapi(gameController)
{
    var app = express.createServer(),
        gc = gameController;

    app.use(express.bodyParser());

    // Middleware to load the playerState when session.playerID is set.
    app.use(function (req, res, next)
    {
        if (!req.session || !req.session.playerID) return next();

        gc.getPlayerState(req.session.playerID, function (playerState)
        {
            req.playerState = playerState;
            next();
        });
    });

    // Parse and load stageID and questionSetID params if they are present in the URL.
    app.param('stageID', function (req, res, next, stageID)
    {
        gc.getStage(stageID, function (stage)
        {
            if (!stage) return next(new Error('Cannot find stage with ID ' + stageID));

            req.stage = stage;
            next();
        });
    });
    app.param('questionSetID', function (req, res, next, questionSetID)
    {
        if (questionSetID == 'next')
        {
            req.stage.getNextQuestionSet(req.playerState, function (questionSet)
            {
                req.questionSet = questionSet;
                next();
            });
        }
        else
        {
            req.stage.getQuestionSet(questionSetID, function (questionSet)
            {
                if (!questionSet) return next(new Error('Cannot find question set with ID ' + questionSet + ' in stage ' + req.stage.id));

                req.questionSet = questionSet;
                next();
            });
        }
    });

    // REST API endpoints that call out to the game controller.

    /*
        Method: GET /stage/:stageID?

        If stageID is specified, sends the results of <GameController.getStage>.  Otherwise, calls <GameController.getAvailableStagesForPlayer> and returns an object with the key `stageIDs' with an array of stage ID strings.
    */
    app.get('/stage/:stageID?', function (req, res, next)
    {
        if (req.stage)
        {
            res.send(req.stage.toJSON());
        }
        else
        {
            gc.getAvailableStagesForPlayer(req.playerState, function (stages)
            {
                res.send({'stages': stages});
            });
        }
    });

    /*
        Method: GET /stage/:stageID/questionSet/:questionSetID?

        If questionSetID is specified, sends the results of <Stage.getQuestionSet>.  Otherwise, calls <Stage.getAllQuestionSetIDs> and returns an object with the key `questionSetIDs' with an array of question set ID strings.
    */
    app.get('/stage/:stageID/questionSet/:questionSetID?', function (req, res, next)
    {
        if (req.params.questionSetID)
        {
            if (req.questionSet) res.send(req.questionSet.toJSON());
            else  res.send({});
        }
        else
        {
            req.stage.getAllQuestionSetIDs(function (questionSetIDs)
            {
                res.send({'questionSetIDs': questionSetIDs});
            });
        }
    });

    /*
        Method: GET /stage/:stageID/questionSet/:questionSetID/engine

        Sends the engine configuration for the given <QuestionSet> by calling <GameController.getGameEngineForQuestionSet>.
    */
    app.get('/stage/:stageID/questionSet/:questionSetID/engine', function (req, res, next)
    {
        gc.getGameEngineForQuestionSet(req.questionSet, function (engine)
        {
            if (!engine) return next(new Error('Cannot find engine for question set ' + req.questionSet.id));

            res.send(engine.toJSON());
        });
    });

    /*
        Method: POST /stage/:stageID/questionSet/:questionSetID/results

        Saves the body of the request as the results for the given question set by calling <GameController.saveQuestionSetResults>.  Sends an empty object on success, or an error object with status 500 on failure.
    */
    app.post('/stage/:stageID/questionSet/:questionSetID/results', function (req, res)
    {
        gc.saveQuestionSetResults(req.playerState, req.questionSet, req.rawBody, function (error)
        {
            if (error)
            {
                console.log('Error saving question set results:');
                console.log(error);
                res.send(error, 500);
            }
            else
            {
                res.send({});
            }
        });
    });

    return app;
};
<commit_msg>Better error handling<commit_after>"use strict";

var express = require('express');

/*
    Title: restapi

    This module creates an expressjs webapp that calls out to the given <GameController> and sends the results back as JSON.  For example, the REST API can be installed within a parent webapp at the sub-URL '/api' by doing:

    | var gc = createMyGameController(...);
    | var app = require('express').createServer();
    | app.use('/api', require('restapi')(gc));

    If the current request's session object has a `playerID' key, the <PlayerState> for that player will be loaded and used as the context.  Alternatively, the parent app can populate the `playerState' property of the request object to get the same effect.

    In general, all data objects are turned into JSON by calling a `toJSON' method.

    * Specs: <http://fluencychallenge.com/wiki/DesignAndImplementation/GameController#RESTAPI>
*/

module.exports = function restapi(gameController)
{
    var app = express.createServer(),
        gc = gameController;

    app.use(express.bodyParser());

    // Middleware to load the playerState when session.playerID is set.
    app.use(function (req, res, next)
    {
        if (!req.session || !req.session.playerID) return next();

        gc.getPlayerState(req.session.playerID, function (playerState)
        {
            req.playerState = playerState;
            next();
        });
    });

    // Parse and load stageID and questionSetID params if they are present in the URL.
    app.param('stageID', function (req, res, next, stageID)
    {
        gc.getStage(stageID, function (stage)
        {
            if (!stage) return next(new Error('Cannot find stage with ID ' + stageID));

            req.stage = stage;
            next();
        });
    });
    app.param('questionSetID', function (req, res, next, questionSetID)
    {
        if (questionSetID == 'next')
        {
            req.stage.getNextQuestionSet(req.playerState, function (questionSet)
            {
                req.questionSet = questionSet;
                next();
            });
        }
        else
        {
            req.stage.getQuestionSet(questionSetID, function (questionSet)
            {
                if (!questionSet) return next(new Error('Cannot find question set with ID ' + questionSet + ' in stage ' + req.stage.id));

                req.questionSet = questionSet;
                next();
            });
        }
    });

    // REST API endpoints that call out to the game controller.

    /*
        Method: GET /stage/:stageID?

        If stageID is specified, sends the results of <GameController.getStage>.  Otherwise, calls <GameController.getAvailableStagesForPlayer> and returns an object with the key `stageIDs' with an array of stage ID strings.
    */
    app.get('/stage/:stageID?', function (req, res, next)
    {
        if (req.stage)
        {
            res.send(req.stage.toJSON());
        }
        else
        {
            gc.getAvailableStagesForPlayer(req.playerState, function (stages)
            {
                res.send({'stages': stages});
            });
        }
    });

    /*
        Method: GET /stage/:stageID/questionSet/:questionSetID?

        If questionSetID is specified, sends the results of <Stage.getQuestionSet>.  Otherwise, calls <Stage.getAllQuestionSetIDs> and returns an object with the key `questionSetIDs' with an array of question set ID strings.
    */
    app.get('/stage/:stageID/questionSet/:questionSetID?', function (req, res, next)
    {
        if (req.params.questionSetID)
        {
            if (req.questionSet) res.send(req.questionSet.toJSON());
            else  res.send({});
        }
        else
        {
            req.stage.getAllQuestionSetIDs(function (questionSetIDs)
            {
                res.send({'questionSetIDs': questionSetIDs});
            });
        }
    });

    /*
        Method: GET /stage/:stageID/questionSet/:questionSetID/engine

        Sends the engine configuration for the given <QuestionSet> by calling <GameController.getGameEngineForQuestionSet>.
    */
    app.get('/stage/:stageID/questionSet/:questionSetID/engine', function (req, res, next)
    {
        gc.getGameEngineForQuestionSet(req.questionSet, function (engine)
        {
            if (!engine) return next(new Error('Cannot find engine for question set ' + req.questionSet.id));

            res.send(engine.toJSON());
        });
    });

    /*
        Method: POST /stage/:stageID/questionSet/:questionSetID/results

        Saves the body of the request as the results for the given question set by calling <GameController.saveQuestionSetResults>.  Sends an empty object on success, or an error object with status 500 on failure.
    */
    app.post('/stage/:stageID/questionSet/:questionSetID/results', function (req, res, next)
    {
        gc.saveQuestionSetResults(req.playerState, req.questionSet, req.rawBody, function (error)
        {
            if (error)
            {
                console.log('Error saving question set results:');
                console.log(error);
                next(error);
            }
            else
            {
                res.send({});
            }
        });
    });

    return app;
};
<|endoftext|>