<commit_before>(function() {
    var getValidMethod = function(method) {
        if (method) {
            var proposedMethod = method.toLowerCase();

            // PUT & POST are the only acceptable methods for now.
            // GET requires us to be able to convert form fields into a URL-encoded string.
            if (['put', 'post'].indexOf(proposedMethod) >= 0) {
                return proposedMethod;
            }
        }
    },

    interceptSubmit = function() {
        this.addEventListener('submit', function(event) {
            // Stop form submission.  Believe it or not,
            // both of these are required for some reason,
            // and returning false doesn't seem to reliably work.
            event.preventDefault();
            event.stopPropagation();

            // respect any field validation attributes
            // NOTE: Safari doesn't have any visual indications when submit is blocked
            if (this.checkValidity()) {
                sendForm.call(this, this);
            }
        }.bind(this));
    },

    listenForAjaxComplete = function() {
        var sender = this.shadowRoot.querySelector('core-ajax');

        sender.addEventListener('core-complete', function(event) {
             this.fire('submitted', event.detail.xhr);
        }.bind(this));
    },

    sendForm = function(form) {
        var sender = this.shadowRoot.querySelector('core-ajax'),
            formData = new FormData(window.unwrap(form)),
            fileInput = form.querySelector('file-input');

        if (fileInput) {
            var fileInputName = fileInput.getAttribute('name') || 'files';

            if (fileInput.files.length > 1) {
                fileInputName += '[]';
            }

            fileInput.files.forEach(function(file) {
                formData.append(fileInputName, file);
            });
        }

        // make sure Polymer/core-ajax doesn't touch the Content-Type.
        // The browser must set this with the proper multipart boundary ID.
        sender.contentType = null;

        if (this.cookies) {
            sender.withCredentials = true;
        }

        sender.body = formData;
        sender.go();
    },

    watchForInvalidFields = function(form) {
        var customEl = this,
            fields = form.children,
            invalidFields = [],
            timer = null;

        for (var i = 0; i < fields.length; i++) {
            /* jshint loopfunc:true */
            fields[i].addEventListener('invalid', function() {
                invalidFields.push(this);

                // In case another element is invalid and the event
                // hasn't been triggered yet, hold off on firing the
                // invalid event on the custom el.
                clearTimeout(timer);
                timer = setTimeout(function() {
                    customEl.fire('invalid', invalidFields);
                    invalidFields = [];
                    console.error('Form submission blocked - constraints violation.');
                }, 10);
            });
        }

    };

    /* globals Polymer */
    /* jshint newcap: false */
    Polymer('ajax-form', {
        acceptableMethod: 'post',

        cookies: false,

        domReady: function() {
            // The method attribute set on the light-DOM `<form>`
            // can't seem to be access as a property of this element,
            // unlike other attributes.  Perhaps due to the fact that
            // we are extending a form and a "natural" form also has a
            // method attr?  Need to look into this further.
            this.acceptableMethod = getValidMethod(this.getAttribute('method'));

            if (!this.acceptableMethod) {
                throw new Error('Invalid method!');
            }

            watchForInvalidFields.call(this, this);

            interceptSubmit.call(this);

            listenForAjaxComplete.call(this);
        }
    });
}());<commit_msg>feat($submitting-event): event just before form is sent closes #10<commit_after>(function() {
    var getValidMethod = function(method) {
        if (method) {
            var proposedMethod = method.toLowerCase();

            // PUT & POST are the only acceptable methods for now.
            // GET requires us to be able to convert form fields into a URL-encoded string.
            if (['put', 'post'].indexOf(proposedMethod) >= 0) {
                return proposedMethod;
            }
        }
    },

    interceptSubmit = function() {
        this.addEventListener('submit', function(event) {
            // Stop form submission.  Believe it or not,
            // both of these are required for some reason,
            // and returning false doesn't seem to reliably work.
            event.preventDefault();
            event.stopPropagation();

            // respect any field validation attributes
            // NOTE: Safari doesn't have any visual indications when submit is blocked
            if (this.checkValidity()) {
                this.fire('submitting');
                sendForm.call(this, this);
            }
        }.bind(this));
    },

    listenForAjaxComplete = function() {
        var sender = this.shadowRoot.querySelector('core-ajax');

        sender.addEventListener('core-complete', function(event) {
             this.fire('submitted', event.detail.xhr);
        }.bind(this));
    },

    sendForm = function(form) {
        var sender = this.shadowRoot.querySelector('core-ajax'),
            formData = new FormData(window.unwrap(form)),
            fileInput = form.querySelector('file-input');

        if (fileInput) {
            var fileInputName = fileInput.getAttribute('name') || 'files';

            if (fileInput.files.length > 1) {
                fileInputName += '[]';
            }

            fileInput.files.forEach(function(file) {
                formData.append(fileInputName, file);
            });
        }

        // make sure Polymer/core-ajax doesn't touch the Content-Type.
        // The browser must set this with the proper multipart boundary ID.
        sender.contentType = null;

        if (this.cookies) {
            sender.withCredentials = true;
        }

        sender.body = formData;
        sender.go();
    },

    watchForInvalidFields = function(form) {
        var customEl = this,
            fields = form.children,
            invalidFields = [],
            timer = null;

        for (var i = 0; i < fields.length; i++) {
            /* jshint loopfunc:true */
            fields[i].addEventListener('invalid', function() {
                invalidFields.push(this);

                // In case another element is invalid and the event
                // hasn't been triggered yet, hold off on firing the
                // invalid event on the custom el.
                clearTimeout(timer);
                timer = setTimeout(function() {
                    customEl.fire('invalid', invalidFields);
                    invalidFields = [];
                    console.error('Form submission blocked - constraints violation.');
                }, 10);
            });
        }

    };

    /* globals Polymer */
    /* jshint newcap: false */
    Polymer('ajax-form', {
        acceptableMethod: 'post',

        cookies: false,

        domReady: function() {
            // The method attribute set on the light-DOM `<form>`
            // can't seem to be access as a property of this element,
            // unlike other attributes.  Perhaps due to the fact that
            // we are extending a form and a "natural" form also has a
            // method attr?  Need to look into this further.
            this.acceptableMethod = getValidMethod(this.getAttribute('method'));

            if (!this.acceptableMethod) {
                throw new Error('Invalid method!');
            }

            watchForInvalidFields.call(this, this);

            interceptSubmit.call(this);

            listenForAjaxComplete.call(this);
        }
    });
}());<|endoftext|>


<commit_before>let browser = typeof window.browser === 'undefined' ? window.chrome : window.browser;

const APP_NAME = 'Norcom Internet Switch';

const STATE_ICONS = [
    {
        19: 'icons/internet-disabled-19.png',
        38: 'icons/internet-disabled-38.png'
    },

    {
        19: 'icons/internet-enabled-19.png',
        38: 'icons/internet-enabled-38.png'
    }
];

const STATE_TITLES = [
    'Включить интернет Norcom',
    'Выключить интернет Norcom'
];

const STATE_CHANGE_NOTIFICATIONS = [
    {
        type: 'basic',
        iconUrl: browser.extension.getURL('icons/internet-disabled-96.png'),
        title: APP_NAME
    },

    {
        type: 'basic',
        iconUrl: browser.extension.getURL('icons/internet-enabled-96.png'),
        title: APP_NAME
    }
];

function main() {
    let stateChangeTimeout;
    let state = 0;

    browser.browserAction.onClicked.addListener(() => {
        state = +!state;

        updateView(state);

        if (stateChangeTimeout) {
            clearTimeout(stateChangeTimeout);
        }

        stateChangeTimeout = setTimeout(() => {
            toggleInternet(state).then((res) => {
                res.text().then((content) => {
                    let message = parseMessage(content);

                    showNotification(state, message);
                });
            });

        }, 500);
    });

    updateView(state);
}

function updateView(state) {
    return [updateIcon(state), updateTitle(state)];
}

function updateIcon(state) {
    let path = STATE_ICONS[state];
    return browser.browserAction.setIcon({path});
}

function updateTitle(state) {
    let title = STATE_TITLES[state];
    return browser.browserAction.setTitle({title});
}

function getEndpointUrl(state) {
    return `http://www.norcom.ru/core/keeper.php?act=keeper_change_status&status=${+!state}`;
}

function toggleInternet(state) {
    let url = getEndpointUrl(state);
    let req = new Request(url, {method: 'GET'});

    return fetch(req);
}

function parseMessage(content) {
    let matches = content.match(/<span[^>]*>(.*?)<\/span>/);

    if (matches && matches[1]) {
        let message = matches[1];

        return message.replace(/(<([^>]+)>)/ig, '');
    }

    return null;
}

function showNotification(state, message) {
    var options = STATE_CHANGE_NOTIFICATIONS[state];
    options.message = message;

    browser.notifications.create('norcom-internet-switch-notification', options);
}

main();<commit_msg>Take into account the connection status<commit_after>let browser = typeof window.browser === 'undefined' ? window.chrome : window.browser;

const APP_NAME = 'Norcom Internet Switch';

const STATE_ICONS = [
    {
        19: 'icons/internet-enabled-19.png',
        38: 'icons/internet-enabled-38.png'
    },

    {
        19: 'icons/internet-disabled-19.png',
        38: 'icons/internet-disabled-38.png'
    },

    {
        19: 'icons/internet-disabled-19.png',
        38: 'icons/internet-disabled-38.png'
    }
];

const STATE_TITLES = [
    'Выключить интернет Norcom',
    'Включить интернет Norcom',
    'Включить интернет Norcom',
];

const STATE_CHANGE_NOTIFICATIONS = [
    {
        type: 'basic',
        iconUrl: browser.extension.getURL('icons/internet-enabled-96.png'),
        title: APP_NAME
    },

    {
        type: 'basic',
        iconUrl: browser.extension.getURL('icons/internet-disabled-96.png'),
        title: APP_NAME
    },

    {
        type: 'basic',
        iconUrl: browser.extension.getURL('icons/internet-error-96.png'),
        title: APP_NAME
    }
];

function main() {
    let stateChangeTimeout;
    var state = 0;

    /* Проверим текущее состояние подключения. Установим соответсвующую иконку и уведомим пользователя. */
    getCurrentState().then((res) => {
        res.text().then((content) => {
            let r = parseResponse(content);
            updateView(r.state);
            showNotification(r.state, r.message);
            state = r.state;
        });
    });
    /* ================================================================================================ */

    browser.browserAction.onClicked.addListener(() => {
        state = +!state;

        if (stateChangeTimeout) {
            clearTimeout(stateChangeTimeout);
        }

        stateChangeTimeout = setTimeout(() => {
            toggleInternet(state).then((res) => {
                res.text().then((content) => {
                    let r = parseResponse(content);

                    /*
                        Сообщение и иконка теперь зависят от возвращаемого результата.
                        Это позволяет избежать странного поведения, когда на счету, например, нет денег
                        или мы подключены к другой сети и кнопка нам недоступна.
                    */
                    showNotification(r.state, r.message);
                    updateView(r.state);

                    /* Обнуляет статус в случае ошибки. Нужно сделать красивее (: */
                    if (r.state > 1) {
                        state = 0;
                    }
                });
            });

        }, 500);
    });

    updateView(state);
}

function updateView(state) {
    return [updateIcon(state), updateTitle(state)];
}

function updateIcon(state) {
    let path = STATE_ICONS[state];
    return browser.browserAction.setIcon({path});
}

function updateTitle(state) {
    let title = STATE_TITLES[state];
    return browser.browserAction.setTitle({title});
}

function getEndpointUrl(state) {
    return `http://www.norcom.ru/core/keeper.php?act=keeper_change_status&status=${state}`;
}

function toggleInternet(state) {
    let url = getEndpointUrl(state);
    let req = new Request(url, {method: 'GET'});

    return fetch(req);
}

function getCurrentState() {
    let url = getEndpointUrl(-1);
    let req = new Request(url, { method: 'GET' });

    return fetch(req);
}

/* Парсим ответ. Выдаем сообщение и установившийся статус соединения. */
function parseResponse(content) {
    /*
        За неимением подробной информации, будем исходить из того, что статус
        может принимать значения 0/1.
        Статус == 2 будет сигнализировать об ошибке.
    */
    let state = 2;
    let message = null;

    let state_match = content.match(/keeper_status.+?(-?\d+)/);

    if (state_match && state_match[1]) {
        state = parseInt(state_match[1]);
    }

    let message_match = content.match(/<span[^>]*>(.*?)<\/span>/);
    if (message_match && message_match[1]) {
        message = message_match[1].replace(/(<([^>]+)>)/ig, '');
    }

    return { state: state, message: message };
}

function showNotification(state, message) {
    var options = STATE_CHANGE_NOTIFICATIONS[state];
    options.message = message;

    browser.notifications.create('norcom-internet-switch-notification', options);
}

main();
<|endoftext|>


<commit_before>import { filter } from 'lodash'

export const unseenThreadCount = (threads, last_viewed_messages_at) =>
                                   filter(threads, t => threadIsUnnoticed(t, last_viewed_messages_at) && threadIsUnread(t)).length

export const threadIsUnnoticed = (thread, last_viewed_messages_at) =>
                                   new Date(last_viewed_messages_at) < new Date(thread.updated_at)

export const threadIsUnread = thread =>
                                new Date(thread.last_read_at) < new Date(thread.updated_at)
<commit_msg>cleanup<commit_after>import { countBy } from 'lodash'

export const unseenThreadCount = (threads, last_viewed_messages_at) =>
  countBy(threads, t =>
    threadIsUnnoticed(t, last_viewed_messages_at) && threadIsUnread(t))

export const threadIsUnnoticed = (thread, last_viewed_messages_at) =>
  new Date(last_viewed_messages_at) < new Date(thread.updated_at)

export const threadIsUnread = thread =>
  new Date(thread.last_read_at) < new Date(thread.updated_at)
<|endoftext|>


<commit_before>// Copyright 2016 David Lavieri.  All rights reserved.
// Use of this source code is governed by a MIT License
// License that can be found in the LICENSE file.

import React, {PropTypes} from 'react'
import {Link} from 'react-router'

const MyLink = ({href, children}) => {
  const url = (typeof href === 'string' && href.length > 0) ? href : '#'

  if (url.indexOf('http') === 0) {
    return {children}
  }

  return <Link to={url}>{children}</Link>
}

MyLink.propTypes = {
  href: PropTypes.string,
  children: PropTypes.node
}

export default MyLink
<commit_msg>remove MyLink Component<commit_after><|endoftext|>


<commit_before>var args = arguments[0] || {};

// fill undefined args with defaults
_.defaults(args, {
    indicatorColor: "#000",
    indicatorHeight: 5,
    tabs: false,
    scrollOffset: 40,
    height: args.tabs ? 48 : 5,
    width: Ti.UI.FILL
});

// additional adjustments for tabs
if (args.tabs) {
    args.tabs = {
      dividerColor: "#ccc",
      width: args.tabWidth
    };
}

// apply properties of Ti.UI.View that can be applied to paging control view
[
    "backgroundColor",
    "backgroundImage",
    "backgroundLeftCap",
    "backgroundRepeat",
    "backgroundTopCap",
    "borderRadius",
    "borderWidth",
    "bottom",
    "height",
    "horizontalWrap",
    "left",
    "opacity",
    "right",
    "top",
    "visible",
    "width",
    "zIndex"
].forEach(function(prop){ _.has(args, prop) && ($.pagingcontrol[prop] = args[prop]); });

// NOTE: THIS DOES NOT WORK ANYMORE WITH ALLOY 1.4.0
// try to find scrollable view as child of parent
// this should work, if pagingcontrol is scrollable view have the same parent
// otherwise you can pass it with args or setScrollableView
if (args.__parentSymbol){
    args.__parentSymbol.children.length > 0 &&
    ($.scrollableView = _.find(args.__parentSymbol.children, function(child){
        return child.apiName === "Ti.UI.ScrollableView";
    }));
}

// assign passed reference of scrollable view
(_.has(args, "scrollableView")) && ($.scrollableView = args.scrollableView);

if ($.scrollableView){
    postLayout(init);
}

/**
 * calls back after postlayout
 * @param {Function} callback
 * @param {Boolean} orientation change  wether called from oc callback
 */
function postLayout(callback, oc){

    if (!oc && $.pagingcontrol.size.width){
        callback();
    }else{
        // wait for postlayout event to get the pagingcontrol size
        $.pagingcontrol.addEventListener('postlayout', function onPostLayout(){

            // callback
            callback();

            // remove eventlistener
            $.pagingcontrol.removeEventListener('postlayout', onPostLayout);
        });
    }
}

/**
 * initialization method
 */
function init(){

    if (args.tabs) {

      // create tabs
      $.tabsCtrl = Widget.createController('tabs', {
        tabs: args.tabs,
        titles: _.map($.scrollableView.getViews(), function(v){ return v.title; })
      });

      // add tabs
      $.pagingcontrol.add($.tabsCtrl.getView());

      // add bottom border
      $.pagingcontrol.add(Ti.UI.createView({
        width: Ti.UI.FILL,
        height: 2,
        bottom: 0,
        backgroundColor: '#ededed'
      }));

      // add tab select listener
      $.tabsCtrl.on('select', function(e){
        e.cancelBubble = true;
        $.trigger('select', { tab: e.tab, view: e.view });
        $.scrollableView.currentPage = e.tab;
        $.indicator.setLeft(e.tab * $.iWidth);
      });
    }

    // create the indicator view
    $.indicator = Ti.UI.createView({
        backgroundColor: args.indicatorColor,
        height: args.indicatorHeight,
        width: Ti.UI.SIZE,
        bottom: 0,
        left: 0,
        zIndex: 2
    });

    adjustePositions();

    // add the indicator
    $.pagingcontrol.add($.indicator);

    // add scroll listener to scrollable view
    $.scrollableView.addEventListener('scroll', onScroll);
    Ti.Gesture.addEventListener('orientationchange', onOrientationChange);
}

/**
 * Callback for scroll event
 */
function onScroll(e){
    e.cancelBubble = true;
    // update the indicator position
    $.indicator.setLeft(e.currentPageAsFloat * $.iWidth);

    args.tabs && updateOffset(e.currentPageAsFloat);
}

/**
 * sets the tab bar offset
 * @param {Number} index
 */
function updateOffset(index){
  var width = $.pagingcontrol.size.width,
  tabsWidth = $.tabsCtrl.getWidth(),
  maxOffset = tabsWidth - width,
  tabSpace = tabsWidth * index / $.scrollableView.views.length,
  measurement = require('alloy/measurement');

  if (width < tabsWidth){

    var offset = tabSpace - args.scrollOffset,
    offsetDp = offset < maxOffset ? offset : maxOffset,
    newOffset = OS_IOS ? (offsetDp < 0 ? 0 : offsetDp) : measurement.dpToPX(offsetDp);

    $.pagingcontrol.setContentOffset(
      { x: newOffset, y: 0},
      { animated: false }
    );
  }

}

/**
 * Callback for orientationchange event
 */
function onOrientationChange(e){
    postLayout(adjustePositions, true);
}

/**
 * Adjust initial layout positions
 */
function adjustePositions() {
  var totalWidth = args.tabs ? $.tabsCtrl.getWidth() : $.pagingcontrol.size.width;
  $.iWidth = Math.floor(totalWidth / $.scrollableView.views.length);
  $.indicator.setWidth($.iWidth);
  $.indicator.setLeft($.scrollableView.getCurrentPage() * $.iWidth);
}

/**
 * if you need to set it in the controller
 * @param {Ti.UI.Scrollableview} scrollable view
 */
exports.setScrollableView = function(_sv){
    $.scrollableView = _sv;
    postLayout(init);
};

/**
 * removes orientationchange Listener
 */
exports.destroy = function(){
    Ti.Gesture.removeEventListener('orientationchange', onOrientationChange);
    args.tabs && $.tabsCtrl.off();
};
<commit_msg>New condition on scroll event<commit_after>var args = arguments[0] || {};

// fill undefined args with defaults
_.defaults(args, {
    indicatorColor: "#000",
    indicatorHeight: 5,
    tabs: false,
    scrollOffset: 40,
    height: args.tabs ? 48 : 5,
    width: Ti.UI.FILL
});

// additional adjustments for tabs
if (args.tabs) {
    args.tabs = {
      dividerColor: "#ccc",
      width: args.tabWidth
    };
}

// apply properties of Ti.UI.View that can be applied to paging control view
[
    "backgroundColor",
    "backgroundImage",
    "backgroundLeftCap",
    "backgroundRepeat",
    "backgroundTopCap",
    "borderRadius",
    "borderWidth",
    "bottom",
    "height",
    "horizontalWrap",
    "left",
    "opacity",
    "right",
    "top",
    "visible",
    "width",
    "zIndex"
].forEach(function(prop){ _.has(args, prop) && ($.pagingcontrol[prop] = args[prop]); });

// NOTE: THIS DOES NOT WORK ANYMORE WITH ALLOY 1.4.0
// try to find scrollable view as child of parent
// this should work, if pagingcontrol is scrollable view have the same parent
// otherwise you can pass it with args or setScrollableView
if (args.__parentSymbol){
    args.__parentSymbol.children.length > 0 &&
    ($.scrollableView = _.find(args.__parentSymbol.children, function(child){
        return child.apiName === "Ti.UI.ScrollableView";
    }));
}

// assign passed reference of scrollable view
(_.has(args, "scrollableView")) && ($.scrollableView = args.scrollableView);

if ($.scrollableView){
    postLayout(init);
}

/**
 * calls back after postlayout
 * @param {Function} callback
 * @param {Boolean} orientation change  wether called from oc callback
 */
function postLayout(callback, oc){

    if (!oc && $.pagingcontrol.size.width){
        callback();
    }else{
        // wait for postlayout event to get the pagingcontrol size
        $.pagingcontrol.addEventListener('postlayout', function onPostLayout(){

            // callback
            callback();

            // remove eventlistener
            $.pagingcontrol.removeEventListener('postlayout', onPostLayout);
        });
    }
}

/**
 * initialization method
 */
function init(){

    if (args.tabs) {

      // create tabs
      $.tabsCtrl = Widget.createController('tabs', {
        tabs: args.tabs,
        titles: _.map($.scrollableView.getViews(), function(v){ return v.title; })
      });

      // add tabs
      $.pagingcontrol.add($.tabsCtrl.getView());

      // add bottom border
      $.pagingcontrol.add(Ti.UI.createView({
        width: Ti.UI.FILL,
        height: 2,
        bottom: 0,
        backgroundColor: '#ededed'
      }));

      // add tab select listener
      $.tabsCtrl.on('select', function(e){
        e.cancelBubble = true;
        $.trigger('select', { tab: e.tab, view: e.view });
        $.scrollableView.currentPage = e.tab;
        $.indicator.setLeft(e.tab * $.iWidth);
      });
    }

    // create the indicator view
    $.indicator = Ti.UI.createView({
        backgroundColor: args.indicatorColor,
        height: args.indicatorHeight,
        width: Ti.UI.SIZE,
        bottom: 0,
        left: 0,
        zIndex: 2
    });

    adjustePositions();

    // add the indicator
    $.pagingcontrol.add($.indicator);

    // add scroll listener to scrollable view
    $.scrollableView.addEventListener('scroll', onScroll);
    Ti.Gesture.addEventListener('orientationchange', onOrientationChange);
}

/**
 * Callback for scroll event
 */
function onScroll(e){
    //paco
    //quiero evitar que se propague el evento pero parece que no va
    e.cancelBubble = true;
    // no hacemos nada si el  origen del evento scroll es el ScrollView principal que se llama pagingcontrol
    if (e.source.id != 'pagingcontrol'){
    	// update the indicator position
    	$.indicator.setLeft(e.currentPageAsFloat * $.iWidth);
    	args.tabs && updateOffset(e.currentPageAsFloat);
    }
}

/**
 * sets the tab bar offset
 * @param {Number} index
 */
function updateOffset(index){
  var width = $.pagingcontrol.size.width,
  tabsWidth = $.tabsCtrl.getWidth(),
  maxOffset = tabsWidth - width,
  tabSpace = tabsWidth * index / $.scrollableView.views.length,
  measurement = require('alloy/measurement');

  if (width < tabsWidth){

    var offset = tabSpace - args.scrollOffset,
    offsetDp = offset < maxOffset ? offset : maxOffset,
    newOffset = OS_IOS ? (offsetDp < 0 ? 0 : offsetDp) : measurement.dpToPX(offsetDp);

    $.pagingcontrol.setContentOffset(
      { x: newOffset, y: 0},
      { animated: false }
    );
  }

}

/**
 * Callback for orientationchange event
 */
function onOrientationChange(e){
    postLayout(adjustePositions, true);
}

/**
 * Adjust initial layout positions
 */
function adjustePositions() {
  var totalWidth = args.tabs ? $.tabsCtrl.getWidth() : $.pagingcontrol.size.width;
  $.iWidth = Math.floor(totalWidth / $.scrollableView.views.length);
  $.indicator.setWidth($.iWidth);
  $.indicator.setLeft($.scrollableView.getCurrentPage() * $.iWidth);
}

/**
 * if you need to set it in the controller
 * @param {Ti.UI.Scrollableview} scrollable view
 */
exports.setScrollableView = function(_sv){
    $.scrollableView = _sv;
    postLayout(init);
};

/**
 * removes orientationchange Listener
 */
exports.destroy = function(){
    Ti.Gesture.removeEventListener('orientationchange', onOrientationChange);
    args.tabs && $.tabsCtrl.off();
};
<|endoftext|>


<commit_before>/**
 * 封装支付面板
 */
PayPanel = Ext.extend( Ext.Panel ,{
	feeStore: null ,
	payForm: null,
	feeGrid:null,
	feeData: null,
	constructor: function(){
		// 缴费信息的表单
		this.payForm = new PayForm(this);
		//实例化fee store
		var fields = [
				'fee_sn','busi_name','fee_text','count_text','optr_name','create_time',
				{name: 'real_pay', type: 'int'}, 'create_done_code',
				'prod_sn', 'begin_date','prod_invalid_date','fee_type','buy_num',
				'user_id','acctitem_id','user_type','busi_code'
			]
		this.feeStore = new Ext.data.JsonStore({
			data: [],
			fields: fields
		});
		var lc = langUtils.main("cashPay.pay.columns");
		var bt = langUtils.main("cashPay.pay.buttons");
		this.paySm = new Ext.grid.CheckboxSelectionModel();
		//费用表格
		this.feeGrid = new Ext.grid.GridPanel({
			title: langUtils.main('cashPay.pay._stayTitle'),
			region: 'center',
			border: false,
			sm: this.paySm,
			columns: [
				this.paySm,
			    { header: lc[0], width: 50,renderer: function(v , md, record , i  ){
					return "
取消
";
				}},
				{ header: lc[1], dataIndex: 'busi_name', width: 80},
				{ header: lc[2], dataIndex: 'fee_text',width: 120, renderer:App.qtipValue},
				{ header: lc[3], dataIndex: 'real_pay', width: 70, xtype: 'moneycolumn'},
				{ header: lc[4], dataIndex: 'prod_sn', width: 70},
				{ header: lc[5], dataIndex: 'count_text', width:150, renderer: App.qtipValue},
				{ header: lc[6], dataIndex: 'buy_num', width: 70},
				{ header: lc[7], dataIndex: 'create_time', width: 130},
				{ header: lc[8], dataIndex: 'fee_sn', width: 80},
				{ header: lc[9], dataIndex: 'create_done_code', width: 80}

			],
			ds: this.feeStore,
			listeners: {
				scope: this,
				rowdblclick: this.doRowClick
			}
		});

		this.realPaySm = new Ext.grid.CheckboxSelectionModel();
		this.realFeeStore = new Ext.data.JsonStore({
			fields: fields
		});
		this.realFeeGrid = new Ext.grid.GridPanel({
			title: langUtils.main('cashPay.pay._title'),
			border: false,
			sm: this.realPaySm,
			columns: [
				this.realPaySm,
				{ header: lc[1], dataIndex: 'busi_name', width: 80},
				{ header: lc[2], dataIndex: 'fee_text',width: 120, renderer:App.qtipValue},
				{ header: lc[3], dataIndex: 'real_pay', width: 70, xtype: 'moneycolumn'},
				{ header: lc[4], dataIndex: 'prod_sn', width: 70},
				{ header: lc[5], dataIndex: 'count_text', width:150, renderer: App.qtipValue},
				{ header: lc[6], dataIndex: 'buy_num', width: 70},
				{ header: lc[7], dataIndex: 'create_time', width: 130},
				{ header: lc[8], dataIndex: 'fee_sn', width: 80},
				{ header: lc[9], dataIndex: 'create_done_code', width: 80}

			],
			ds: this.realFeeStore,
			listeners: {
				scope: this,
				rowdblclick: this.doRealRowClick
			}
		});

		PayPanel.superclass.constructor.call(this,{
			id:'PayPanelId',
			border: false,
			layout: 'border',
			items: [

				{
					region: 'center',
					layout: 'border',
					border: false,
					items:[{
						region: 'center',
						layout: 'border',
						items: [this.feeGrid ],
						bbar:['-',
							{text: '确认', iconCls: 'icon-confirm', scope:this, handler: this.doConfirm},'-',
							{text: '移除', iconCls: 'icon-del', scope:this, handler: this.doDelete},'-']
					},
					{
						region: 'south',
						layout: 'fit',
						height: 200,
						items: [this.realFeeGrid ]
					}]
				},{
				region: 'east',
				split: true,
				layout: 'fit',
				width: 230,
				items: this.payForm
			}],
			buttons: [{
				text: bt[0],
				iconCls: 'icon-save',
				width: 100,
				scope: this,
				handler: this.doSave
			}]
		});
		this.loadBaseData();
	},
	loadBaseData: function(){
		// 请求后台的数据
		Ext.Ajax.request({
			scope: this,
			url: Constant.ROOT_PATH + "/core/x/Pay!queryUnPayDetail.action",
			params: {cust_id: App.getCustId()},
			success: function(res, ops){
				var data = Ext.decode(res.responseText);
				this.feeStore.loadData(data["records"]);
				this.feeData = data["simpleObj"];
				Ext.getCmp("labelExchange").setText("1USD=" + this.feeData["EXC"] + "KHR");
				/*var dollar = this.feeData["FEE"]/100.0;
				Ext.getCmp("nfDollar").setValue(dollar);
				Ext.getCmp("nfJianYuan").setValue(0);
				var substrIndex = 1, append = "";
				if(dollar < 0){
					substrIndex = 2;
					append = "-";
				}
				Ext.getCmp("labelDollor").setText(append+ Ext.util.Format.usMoney(dollar).substr(substrIndex));
				var jianYuan = Ext.util.Format.usMoney(dollar * this.feeData["EXC"]);
				Ext.getCmp("LabelJian").setText(append+jianYuan.substr(substrIndex));
				Ext.getCmp("labelExchange").setText("1USD=" + this.feeData["EXC"] + "KHR");
				Ext.getCmp("hdExchange").setValue(this.feeData["EXC"]);*/
			}
		});
	},
	doConfirm: function(){
		var records = this.paySm.getSelections();
		if(records.length > 0){
			if(records.length == this.feeStore.getCount()){
				this.realFeeStore.add(records);
				this.doCalFee();
			}else{
				var recordArray = this.getSameTypeProd(this.feeStore, records);
				this.realFeeStore.add(recordArray);
				this.doCalFee();
			}
		}
	},
	doDelete: function(){
		var records = this.realPaySm.getSelections();
		if(records.length > 0){
			if(records.length == this.realFeeStore.getCount()){
				this.realFeeStore.removeAll();
				this.doCalFee();
			}else{
				var recordArray = this.getSameTypeProd(this.feeStore, records);
				this.realFeeStore.remove(recordArray);
				this.doCalFee();
			}
		}
	},
	doRowClick: function(grid, rowIndex){
		var record = this.feeStore.getAt(rowIndex);
		if(this.realFeeStore.indexOf(record) > 0)
			return;
		var recordArray = this.getSameTypeProd(this.feeStore, record);

		this.realFeeStore.add(recordArray);
		this.doCalFee();
	},
	doRealRowClick: function(grid, rowIndex){
		var record = this.realFeeStore.getAt(rowIndex);
		if(this.realFeeStore.indexOf(record) > 0)
			return;
		var recordArray = this.getSameTypeProd(this.realFeeStore, record);

		this.realFeeStore.remove(recordArray);
		this.doCalFee();
	},
	doCalFee: function(){
		var dollar = 0;
		this.realFeeStore.each(function(record){
			dollar += record.get('real_pay');
		}, this);
		dollar = dollar / 100.0;
		Ext.getCmp("nfDollar").setValue(dollar);
		Ext.getCmp("nfJianYuan").setValue(0);
		var substrIndex = 1, append = "";
		if(dollar < 0){
			substrIndex = 2;
			append = "-";
		}
		Ext.getCmp("labelDollor").setText(append+ Ext.util.Format.usMoney(dollar).substr(substrIndex));
		var jianYuan = Ext.util.Format.usMoney(dollar * this.feeData["EXC"]);
		Ext.getCmp("LabelJian").setText(append+jianYuan.substr(substrIndex));
		Ext.getCmp("labelExchange").setText("1USD=" + this.feeData["EXC"] + "KHR");
		Ext.getCmp("hdExchange").setValue(this.feeData["EXC"]);
	},
	getSameTypeProd: function(store, records){
		var recordArray = [];
		Ext.each(records, function(record){
			var userType = record.get('user_type');
			var prodId = record.get('acctitem_id');
			var userId = record.get('user_id')
			var feeSn = record.get('fee_sn');
			var busiCode = record.get('busi_code');
			if(userType == 'BAND' && prodId){
				//相同用户的宽带订单必须一起支付
				store.each(function(r){
					if(userId == r.get('user_id')){
						recordArray.push(r);
					}
				}, this);
			}else if(busiCode == '1015'){
				//套餐订单，必须一起支付
				store.each(function(r){
					if(r.get('busi_code') == busiCode){
						recordArray.push(r);
					}
				}, this);
			}else{
				//相同用户相同产品必须一起支付
				store.each(function(r){
					if(userId == r.get('user_id') && prodId == r.get('acctitem_id')){
						recordArray.push(r);
					}
				}, this);
			}
		}, this);

		return recordArray;
	},
	deletePay : function() {
		var rec = this.feeGrid.getSelectionModel().getSelected();
		var thiz = this;
		var func = function(flag){
			var comomonParams = App.getValues();
			comomonParams["busiCode"] = "1113";
			var params ={};
			params[CoreConstant.JSON_PARAMS] = Ext.encode(comomonParams);
			Ext.apply(params,{
				fee_sn : rec.get('fee_sn'),
				fee_type : rec.get('fee_type'),
				onlyShowInfo : flag
			});
			var info = "";
			Ext.Ajax.request({
				scope: thiz,
				url: Constant.ROOT_PATH + "/core/x/Pay!cancelUnPayFee.action",
				params: params,
				async : false,
				success: function(res, ops){
					info = Ext.decode(res.responseText);
					if(!flag){
						this.loadBaseData();
						App.getApp().refreshPayInfo(parent);
					}
				}
			});
			return info;
		}
		Confirm( func(true), this ,function(){
			func(false);
		});
	},
	doSaveAndPrint: function(){
		this.doSave(params);
	},
	doSave: function(){
		if(!this.payForm.getForm().isValid()){
			Alert("含有验证不通过的输入项");
			return ;
		}
		Confirm("确定要保存信息吗?", this, function(){
			var params = this.getValues()
			var comomonParams = App.getValues();
			comomonParams["busiCode"] = "1207";
			params[CoreConstant.JSON_PARAMS] = Ext.encode(comomonParams);

			var feeSns = [];
			this.realFeeStore.each(function(record){
				feeSns.push(record.get('fee_sn'));
			},this);
			params['feeSns'] = feeSns;
			console.log(feeSns);

			var mb = Show();
			//提交
			Ext.Ajax.request({
				scope: this,
				params: params,
				url: Constant.ROOT_PATH + "/core/x/Pay!savePayNew.action",
				success: function( res, ops){
					var o = Ext.decode( res.responseText );
					mb.hide();
					if(o["exception"]){
						new DebugWindow( o , "btnBusiSave").show();
					} else {
						Alert('支付成功!',function(){
								App.getApp().refreshPayInfo(parent);
								this.success();
							},this);
					}
				}
			});
		});
	},
	getValues: function(){
		var f = this.payForm.getForm();
		var all = f.getValues();
		all["pay.khr"] = all["pay.khr"] * 100;
		all["pay.usd"] = all["pay.usd"] * 100;
		all["pay.cust_id"] = App.getApp().data.custFullInfo.cust.cust_id;
		return all;
	},
	getFee: function(){
		return 0;
	},
	success:function(){
		App.getApp().menu.bigWindow.show({ text: '发票打印',  attrs: {busiCode:'1068',
					url: 'pages/business/pay/Print.jsp?type=through'}} ,{width: 710, height: 460});
	}
});

Ext.onReady(function(){
	var pay = new PayPanel();
	TemplateFactory.gViewport(pay);
});<commit_msg><commit_after>/**
 * 封装支付面板
 */
PayPanel = Ext.extend( Ext.Panel ,{
	feeStore: null ,
	payForm: null,
	feeGrid:null,
	feeData: null,
	constructor: function(){
		// 缴费信息的表单
		this.payForm = new PayForm(this);
		//实例化fee store
		var fields = [
				'fee_sn','busi_name','fee_text','count_text','optr_name','create_time',
				{name: 'real_pay', type: 'int'}, 'create_done_code',
				'prod_sn', 'begin_date','prod_invalid_date','fee_type','buy_num',
				'user_id','acctitem_id','user_type','busi_code'
			]
		this.feeStore = new Ext.data.JsonStore({
			data: [],
			fields: fields
		});
		var lc = langUtils.main("cashPay.pay.columns");
		var bt = langUtils.main("cashPay.pay.buttons");
		this.paySm = new Ext.grid.CheckboxSelectionModel();
		//费用表格
		this.feeGrid = new Ext.grid.GridPanel({
			title: langUtils.main('cashPay.pay._stayTitle'),
			region: 'center',
			border: false,
			sm: this.paySm,
			columns: [
				this.paySm,
			    { header: lc[0], width: 50,renderer: function(v , md, record , i  ){
					return "
取消
";
				}},
				{ header: lc[1], dataIndex: 'busi_name', width: 80},
				{ header: lc[2], dataIndex: 'fee_text',width: 120, renderer:App.qtipValue},
				{ header: lc[3], dataIndex: 'real_pay', width: 70, xtype: 'moneycolumn'},
				{ header: lc[4], dataIndex: 'prod_sn', width: 70},
				{ header: lc[5], dataIndex: 'count_text', width:150, renderer: App.qtipValue},
				{ header: lc[6], dataIndex: 'buy_num', width: 70},
				{ header: lc[7], dataIndex: 'create_time', width: 130},
				{ header: lc[8], dataIndex: 'fee_sn', width: 80},
				{ header: lc[9], dataIndex: 'create_done_code', width: 80}

			],
			ds: this.feeStore,
			listeners: {
				scope: this,
				rowdblclick: this.doRowClick
			}
		});

		this.realPaySm = new Ext.grid.CheckboxSelectionModel();
		this.realFeeStore = new Ext.data.JsonStore({
			fields: fields
		});
		this.realFeeGrid = new Ext.grid.GridPanel({
			title: langUtils.main('cashPay.pay._title'),
			border: false,
			sm: this.realPaySm,
			columns: [
				this.realPaySm,
				{ header: lc[1], dataIndex: 'busi_name', width: 80},
				{ header: lc[2], dataIndex: 'fee_text',width: 120, renderer:App.qtipValue},
				{ header: lc[3], dataIndex: 'real_pay', width: 70, xtype: 'moneycolumn'},
				{ header: lc[4], dataIndex: 'prod_sn', width: 70},
				{ header: lc[5], dataIndex: 'count_text', width:150, renderer: App.qtipValue},
				{ header: lc[6], dataIndex: 'buy_num', width: 70},
				{ header: lc[7], dataIndex: 'create_time', width: 130},
				{ header: lc[8], dataIndex: 'fee_sn', width: 80},
				{ header: lc[9], dataIndex: 'create_done_code', width: 80}

			],
			ds: this.realFeeStore,
			listeners: {
				scope: this,
				rowdblclick: this.doRealRowClick
			}
		});

		PayPanel.superclass.constructor.call(this,{
			id:'PayPanelId',
			border: false,
			layout: 'border',
			items: [

				{
					region: 'center',
					layout: 'border',
					border: false,
					items:[{
						region: 'center',
						layout: 'border',
						items: [this.feeGrid ],
						bbar:['-',
							{text: '确认', iconCls: 'icon-confirm', scope:this, handler: this.doConfirm},'-',
							{text: '移除', iconCls: 'icon-del', scope:this, handler: this.doDelete},'-']
					},
					{
						region: 'south',
						layout: 'fit',
						height: 200,
						items: [this.realFeeGrid ]
					}]
				},{
				region: 'east',
				split: true,
				layout: 'fit',
				width: 230,
				items: this.payForm
			}],
			buttons: [{
				text: bt[0],
				iconCls: 'icon-save',
				width: 100,
				scope: this,
				handler: this.doSave
			}]
		});
		this.loadBaseData();
	},
	loadBaseData: function(){
		// 请求后台的数据
		Ext.Ajax.request({
			scope: this,
			url: Constant.ROOT_PATH + "/core/x/Pay!queryUnPayDetail.action",
			params: {cust_id: App.getCustId()},
			success: function(res, ops){
				var data = Ext.decode(res.responseText);
				this.feeStore.loadData(data["records"]);
				this.feeData = data["simpleObj"];
				Ext.getCmp("labelExchange").setText("1USD=" + this.feeData["EXC"] + "KHR");
				/*var dollar = this.feeData["FEE"]/100.0;
				Ext.getCmp("nfDollar").setValue(dollar);
				Ext.getCmp("nfJianYuan").setValue(0);
				var substrIndex = 1, append = "";
				if(dollar < 0){
					substrIndex = 2;
					append = "-";
				}
				Ext.getCmp("labelDollor").setText(append+ Ext.util.Format.usMoney(dollar).substr(substrIndex));
				var jianYuan = Ext.util.Format.usMoney(dollar * this.feeData["EXC"]);
				Ext.getCmp("LabelJian").setText(append+jianYuan.substr(substrIndex));
				Ext.getCmp("labelExchange").setText("1USD=" + this.feeData["EXC"] + "KHR");
				Ext.getCmp("hdExchange").setValue(this.feeData["EXC"]);*/
			}
		});
	},
	doConfirm: function(){
		var records = this.paySm.getSelections();
		if(records.length > 0){
			if(records.length == this.feeStore.getCount()){
				this.realFeeStore.add(records);
				this.doCalFee();
			}else{
				var recordArray = this.getSameTypeProd(this.feeStore, records);
				this.realFeeStore.add(recordArray);
				this.doCalFee();
			}
		}
	},
	doDelete: function(){
		var records = this.realPaySm.getSelections();
		if(records.length > 0){
			if(records.length == this.realFeeStore.getCount()){
				this.realFeeStore.removeAll();
				this.doCalFee();
			}else{
				var recordArray = this.getSameTypeProd(this.feeStore, records);
				this.realFeeStore.remove(recordArray);
				this.doCalFee();
			}
		}
	},
	doRowClick: function(grid, rowIndex){
		var record = this.feeStore.getAt(rowIndex);
		if(this.realFeeStore.indexOf(record) > 0)
			return;
		var recordArray = this.getSameTypeProd(this.feeStore, record);

		this.realFeeStore.add(recordArray);
		this.doCalFee();
	},
	doRealRowClick: function(grid, rowIndex){
		var record = this.realFeeStore.getAt(rowIndex);
		if(this.realFeeStore.indexOf(record) > 0)
			return;
		var recordArray = this.getSameTypeProd(this.realFeeStore, record);

		this.realFeeStore.remove(recordArray);
		this.doCalFee();
	},
	doCalFee: function(){
		var dollar = 0;
		this.realFeeStore.each(function(record){
			dollar += record.get('real_pay');
		}, this);
		dollar = dollar / 100.0;
		Ext.getCmp("nfDollar").setValue(dollar);
		Ext.getCmp("nfJianYuan").setValue(0);
		var substrIndex = 1, append = "";
		if(dollar < 0){
			substrIndex = 2;
			append = "-";
		}
		Ext.getCmp("labelDollor").setText(append+ Ext.util.Format.usMoney(dollar).substr(substrIndex));
		var jianYuan = Ext.util.Format.usMoney(dollar * this.feeData["EXC"]);
		Ext.getCmp("LabelJian").setText(append+jianYuan.substr(substrIndex));
		Ext.getCmp("labelExchange").setText("1USD=" + this.feeData["EXC"] + "KHR");
		Ext.getCmp("hdExchange").setValue(this.feeData["EXC"]);
	},
	getSameTypeProd: function(store, records){
		var recordArray = [];
		Ext.each(records, function(record){
			var userType = record.get('user_type');
			var prodId = record.get('acctitem_id');
			var userId = record.get('user_id')
			var feeSn = record.get('fee_sn');
			var busiCode = record.get('busi_code');
			if(userType == 'BAND' && prodId){
				//相同用户的宽带订单必须一起支付
				store.each(function(r){
					if(userId == r.get('user_id')){
						recordArray.push(r);
					}
				}, this);
			}else if(busiCode == '1015'){
				//套餐订单，必须一起支付
				store.each(function(r){
					if(r.get('busi_code') == busiCode){
						recordArray.push(r);
					}
				}, this);
			}else{
				//相同用户相同产品必须一起支付
				store.each(function(r){
					if(userId == r.get('user_id') && prodId == r.get('acctitem_id')){
						recordArray.push(r);
					}
				}, this);
			}
		}, this);

		return recordArray;
	},
	deletePay : function() {
		var rec = this.feeGrid.getSelectionModel().getSelected();
		var thiz = this;
		var func = function(flag){
			var comomonParams = App.getValues();
			comomonParams["busiCode"] = "1113";
			var params ={};
			params[CoreConstant.JSON_PARAMS] = Ext.encode(comomonParams);
			Ext.apply(params,{
				fee_sn : rec.get('fee_sn'),
				fee_type : rec.get('fee_type'),
				onlyShowInfo : flag
			});
			var info = "";
			Ext.Ajax.request({
				scope: thiz,
				url: Constant.ROOT_PATH + "/core/x/Pay!cancelUnPayFee.action",
				params: params,
				async : false,
				success: function(res, ops){
					info = Ext.decode(res.responseText);
					if(!flag){
						this.loadBaseData();
						App.getApp().refreshPayInfo(parent);
					}
				}
			});
			return info;
		}
		Confirm( func(true), this ,function(){
			func(false);
		});
	},
	doSaveAndPrint: function(){
		this.doSave(params);
	},
	doSave: function(){
		if(!this.payForm.getForm().isValid()){
			Alert("含有验证不通过的输入项");
			return ;
		}
		Confirm("确定要保存信息吗?", this, function(){
			var params = this.getValues()
			var comomonParams = App.getValues();
			comomonParams["busiCode"] = "1207";
			params[CoreConstant.JSON_PARAMS] = Ext.encode(comomonParams);

			var feeSns = [];
			this.realFeeStore.each(function(record){
				feeSns.push(record.get('fee_sn'));
			},this);
			params['feeSn'] = feeSns;
			console.log(feeSns);

			var mb = Show();
			//提交
			Ext.Ajax.request({
				scope: this,
				params: params,
				url: Constant.ROOT_PATH + "/core/x/Pay!savePayNew.action",
				success: function( res, ops){
					var o = Ext.decode( res.responseText );
					mb.hide();
					if(o["exception"]){
						new DebugWindow( o , "btnBusiSave").show();
					} else {
						Alert('支付成功!',function(){
								App.getApp().refreshPayInfo(parent);
								this.success();
							},this);
					}
				}
			});
		});
	},
	getValues: function(){
		var f = this.payForm.getForm();
		var all = f.getValues();
		all["pay.khr"] = all["pay.khr"] * 100;
		all["pay.usd"] = all["pay.usd"] * 100;
		all["pay.cust_id"] = App.getApp().data.custFullInfo.cust.cust_id;
		return all;
	},
	getFee: function(){
		return 0;
	},
	success:function(){
		App.getApp().menu.bigWindow.show({ text: '发票打印',  attrs: {busiCode:'1068',
					url: 'pages/business/pay/Print.jsp?type=through'}} ,{width: 710, height: 460});
	}
});

Ext.onReady(function(){
	var pay = new PayPanel();
	TemplateFactory.gViewport(pay);
});<|endoftext|>


<commit_before>/**
 * @file Stats
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 */

import Signal from "../../lib/signals.es6.js";

function Stats(){

    this.signals = {

        updated: new Signal(),

    };

    this.begin();

    this.maxDuration = -Infinity;
    this.minDuration = Infinity;
    this.avgDuration = 14;
    this.lastDuration = Infinity;

    this.prevFpsTime = 0;
    this.lastFps = Infinity;
    this.lastFrames = 1;
    this.frames = 0;
    this.count = 0;

}

Stats.prototype = {

    update: function(){

        this.startTime = this.end();
        this.signals.updated.dispatch();

    },

    begin: function(){

        this.startTime = performance.now();
        this.lastFrames = this.frames;

    },

    end: function(){

        var time = performance.now();

        this.count += 1;
        this.frames += 1;

        this.lastDuration = time - this.startTime;
        this.minDuration = Math.min( this.minDuration, this.lastDuration );
        this.maxDuration = Math.max( this.maxDuration, this.lastDuration );
        this.avgDuration -= this.avgDuration / 30;
        this.avgDuration += this.lastDuration / 30;

        if( time > this.prevFpsTime + 1000 ) {
            this.lastFps = this.frames;
            this.prevFpsTime = time;
            this.frames = 0;
        }

        return time;

    }

};

export default Stats;
<commit_msg>added currentTime to stats object<commit_after>/**
 * @file Stats
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 */

import Signal from "../../lib/signals.es6.js";

function Stats(){

    this.signals = {

        updated: new Signal(),

    };

    this.begin();

    this.maxDuration = -Infinity;
    this.minDuration = Infinity;
    this.avgDuration = 14;
    this.lastDuration = Infinity;

    this.prevFpsTime = 0;
    this.lastFps = Infinity;
    this.lastFrames = 1;
    this.frames = 0;
    this.count = 0;

}

Stats.prototype = {

    update: function(){

        this.startTime = this.end();
        this.currentTime = this.startTime;
        this.signals.updated.dispatch();

    },

    begin: function(){

        this.startTime = performance.now();
        this.lastFrames = this.frames;

    },

    end: function(){

        var time = performance.now();

        this.count += 1;
        this.frames += 1;

        this.lastDuration = time - this.startTime;
        this.minDuration = Math.min( this.minDuration, this.lastDuration );
        this.maxDuration = Math.max( this.maxDuration, this.lastDuration );
        this.avgDuration -= this.avgDuration / 30;
        this.avgDuration += this.lastDuration / 30;

        if( time > this.prevFpsTime + 1000 ) {
            this.lastFps = this.frames;
            this.prevFpsTime = time;
            this.frames = 0;
        }

        return time;

    }

};

export default Stats;
<|endoftext|>


<commit_before>export const SPELL_ID_RULE_OF_LAW = 214202;

const ABILITIES_AFFECTED_BY_MASTERY = [
  225311, // Light of Dawn
  20473, // Holy Shock
  82326, // Holy Light
  19750, // Flash of Light
  196917, // Light of the Martyr
  114165, // Holy Prism
  114158, // Light's Hammer
  183811, // Judgment of Light
  200652, // Tyr's Deliverance
  223306, // Bestow Faith
];

class CombatLogParser {
  lastCast = null;
  // Tracking total healing seen as a sanity check
  totalHealing = 0;

  players = {};
  masteryHealEvents = [];

  player = null;
  playerMasteryPerc = null;
  fight = null;

  get fightDuration() {
    return this.fight.end_time - this.fight.start_time;
  }

  constructor(player, fight) {
    this.player = player;
    this.fight = fight;
  }

  parseEvents(events) {
    return new Promise((resolve, reject) => {
      events.forEach(event => {
        const methodName = `parse_${event.type}`;
        const method = this[methodName];
        if (method) {
          method.call(this, event);
        } else {
          // Report unrecognized events
          // console.warn("Didn't recognize", event.type, event.ability && event.ability.name, event);
        }
      });

      resolve(events.length);
    });
  }

  parse_cast(event) {
    if (this.byPlayer(event)) {
      this.playerEvent(event);
    }
  }
  parse_damage(event) {
    if (this.toPlayer(event)) {
      // Damage coordinates are for the target, so they are only accurate when done TO player
      this.playerEvent(event);
    }
  }
  parse_energize(event) {
    if (this.toPlayer(event)) {
      this.playerEvent(event);
    }
  }
  parse_heal(event) {
    if (this.toPlayer(event)) {
      // Do this before checking if this was done by player so that self-heals will apply full mastery properly
      this.playerEvent(event);
    }
    if (this.byPlayer(event)) {
      if (!this.lastCast) {
        console.error('Received a heal before we detected a cast. Can\'t process since player location is still unknown.', event);
        return;
      } else if (this.playerMasteryPerc === null) {
        console.error('Received a heal before finding out player\'s mastery percentage.', event);
        return;
      }
      const isAbilityAffectedByMastery = ABILITIES_AFFECTED_BY_MASTERY.indexOf(event.ability.guid) !== -1;

      // The actual heal as shown in the log
      const healingDone = event.amount;

      if (isAbilityAffectedByMastery) {
        // console.log(event.ability.name,
        //   `healing:${event.amount},distance:${distance},hasRuleOfLaw:${hasRuleOfLaw},masteryEffectiveness:${masteryEffectiveness}`,
        //   `playerMasteryPerc:${this.playerMasteryPerc}`, event);

        const distance = CombatLogParser.calculateDistance(this.lastCast.x, this.lastCast.y, event.x, event.y) / 100;
        const hasRuleOfLaw = this.hasBuff(SPELL_ID_RULE_OF_LAW);
        // We calculate the mastery effectiveness of this *one* heal
        const masteryEffectiveness = CombatLogParser.calculateMasteryEffectiveness(distance, hasRuleOfLaw);

        // The base healing of the spell (excluding any healing added by mastery)
        const baseHealingDone = healingDone / (1 + this.playerMasteryPerc * masteryEffectiveness);
        const masteryHealingDone = healingDone - baseHealingDone;
        // The max potential mastery healing if we had a mastery effectiveness of 100% on this spell. This does NOT include the base healing
        // Example: a heal that did 1,324 healing with 32.4% mastery with 100% mastery effectiveness will have a max potential mastery healing of 324.
        const maxPotentialMasteryHealing = baseHealingDone * this.playerMasteryPerc; // * 100% mastery effectiveness

        this.masteryHealEvents.push({
          ...event,
          distance,
          masteryEffectiveness,
          baseHealingDone,
          masteryHealingDone,
          maxPotentialMasteryHealing,
        });
      }
      this.totalHealing += healingDone;
    }
  }
  parse_combatantinfo(event) {
    console.log('combatantinfo', event);

    this.players[event.sourceID] = event;

    if (this.byPlayer(event)) {
      this.playerMasteryPerc = CombatLogParser.calculateMasteryPercentage(event.mastery);

      event.auras.forEach(aura => {
        this.applyActiveBuff({
          ability: {
            abilityIcon: aura.icon,
            guid: aura.ability,
          },
          sourceID: aura.source,
        });
      });
    }
  }
  parse_absorbed(event) {
    if (this.byPlayer(event)) {
      this.totalHealing += event.amount;
    }
  }

  byPlayer(event) {
    return (event.sourceID === this.player.id);
  }
  toPlayer(event) {
    return (event.targetID === this.player.id);
  }
  playerEvent(event) {
    if (!event.x || !event.y) {
      return;
    }
    this.verifyCast(event);
    this.lastCast = event;
  }
  verifyCast(event) {
    if (!event.x || !event.y || !this.lastCast) {
      return;
    }
    const distance = CombatLogParser.calculateDistance(this.lastCast.x, this.lastCast.y, event.x, event.y) / 100;
    const timeSince = event.timestamp - this.lastCast.timestamp;
    const maxDistance = Math.max(1, timeSince / 1000 * 10 * 1.1); // 10 yards per second + 10% margin of error
    if (distance > maxDistance) {
      console.warn('Distance since previous event (' + (Math.round(timeSince / 100) / 10) + 's ago) was ' + (Math.round(distance * 10) / 10) + ' yards:', event.type, event, this.lastCast.type, this.lastCast);
    }
  }

  // region Buffs

  /**
   * Contains all buffs which are active right now.
   * @type {Array}
   */
  activeBuffs = [];
  /**
   * Keeps track of all buffs that have been applied during the fight.
   * @type {Array}
   */
  buffHistory = [];

  hasBuff(buffAbilityId) {
    return this.activeBuffs.find(buff => buff.ability.guid === buffAbilityId) !== undefined;
  }
  getBuffUptime(buffAbilityId) {
    return this.buffHistory.reduce((uptime, buff) => {
      if (buff.ability.guid === buffAbilityId) {
        uptime += buff.uptime;
      }
      return uptime;
    }, 0);
  }

  applyActiveBuff(buff) {
    this.activeBuffs.push(buff);
  }
  removeActiveBuff(activeBuff) {
    const activeBuffIndex = this.activeBuffs.findIndex(buff => buff.ability.guid === activeBuff.ability.guid);
    let appliedAtTimestamp = null;
    if (activeBuffIndex !== -1) {
      const removedBuff = this.activeBuffs.splice(activeBuffIndex, 1)[0];
      appliedAtTimestamp = removedBuff.timestamp;
    } else {
      console.error('Tried to remove buff', activeBuff, ', but couldn\'t find it in activeBuffs.');
      appliedAtTimestamp = this.fight.start_time;
    }

    this.buffHistory.push({
      ...activeBuff,
      uptime: activeBuff.timestamp - appliedAtTimestamp,
    });
  }

  parse_applybuff(event) {
    if (!this.toPlayer(event)) return;

    this.applyActiveBuff(event);
  }
  parse_refreshbuff(event) {
    if (!this.toPlayer(event)) return;

    this.removeActiveBuff(event);
    this.applyActiveBuff(event);
  }
  parse_removebuff(event) {
    if (!this.toPlayer(event)) return;

    this.removeActiveBuff(event);
  }

  // endregion

  static calculateDistance(x1, y1, x2, y2) {
    return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
  }
  static calculateMasteryEffectiveness(distance, hasRuleOfLaw) {
    const fullEffectivenessRadius = hasRuleOfLaw ? 15 : 10;
    const falloffRadius = hasRuleOfLaw ? 60 : 40;

    return Math.min(1, Math.max(0, 1 - (distance - fullEffectivenessRadius) / (falloffRadius - fullEffectivenessRadius)));
  }
  static calculateMasteryPercentage(masteryRating) {
    return 0.12 + masteryRating / 26667;
  }
}

export default CombatLogParser;
<commit_msg>Fix a bug where some abilities are not being recognized properly as affecting mastery.<commit_after>export const SPELL_ID_RULE_OF_LAW = 214202;

const ABILITIES_AFFECTED_BY_MASTERY = [
  225311, // Light of Dawn (heal)
  85222, // Light of Dawn (cast)
  25914, // Holy Shock (heal)
  20473, // Holy Shock (cast)
  82326, // Holy Light
  19750, // Flash of Light
  183998, // Light of the Martyr (WCL)
  196917, // Light of the Martyr
  114852, // Holy Prism (heal)
  114165, // Holy Prism (cast)
  119952, // Light's Hammer (heal)
  114158, // Light's Hammer (cast)
  183811, // Judgment of Light (heal)
  200654, // Tyr's Deliverance (heal)
  200652, // Tyr's Deliverance (cast)
  223306, // Bestow Faith
];

class CombatLogParser {
  lastCast = null;
  // Tracking total healing seen as a sanity check
  totalHealing = 0;

  players = {};
  masteryHealEvents = [];

  player = null;
  playerMasteryPerc = null;
  fight = null;

  get fightDuration() {
    return this.fight.end_time - this.fight.start_time;
  }

  constructor(player, fight) {
    this.player = player;
    this.fight = fight;
  }

  parseEvents(events) {
    return new Promise((resolve, reject) => {
      events.forEach(event => {
        const methodName = `parse_${event.type}`;
        const method = this[methodName];
        if (method) {
          method.call(this, event);
        } else {
          // Report unrecognized events
          // console.warn("Didn't recognize", event.type, event.ability && event.ability.name, event);
        }
      });

      resolve(events.length);
    });
  }

  parse_cast(event) {
    if (this.byPlayer(event)) {
      this.playerEvent(event);
    }
  }
  parse_damage(event) {
    if (this.toPlayer(event)) {
      // Damage coordinates are for the target, so they are only accurate when done TO player
      this.playerEvent(event);
    }
  }
  parse_energize(event) {
    if (this.toPlayer(event)) {
      this.playerEvent(event);
    }
  }
  parse_heal(event) {
    if (this.toPlayer(event)) {
      // Do this before checking if this was done by player so that self-heals will apply full mastery properly
      this.playerEvent(event);
    }
    if (this.byPlayer(event)) {
      if (!this.lastCast) {
        console.error('Received a heal before we detected a cast. Can\'t process since player location is still unknown.', event);
        return;
      } else if (this.playerMasteryPerc === null) {
        console.error('Received a heal before finding out player\'s mastery percentage.', event);
        return;
      }
      const isAbilityAffectedByMastery = ABILITIES_AFFECTED_BY_MASTERY.indexOf(event.ability.guid) !== -1;
      console.log(isAbilityAffectedByMastery, event.ability.name, event);

      // The actual heal as shown in the log
      const healingDone = event.amount;

      if (isAbilityAffectedByMastery) {
        // console.log(event.ability.name,
        //   `healing:${event.amount},distance:${distance},hasRuleOfLaw:${hasRuleOfLaw},masteryEffectiveness:${masteryEffectiveness}`,
        //   `playerMasteryPerc:${this.playerMasteryPerc}`, event);

        const distance = CombatLogParser.calculateDistance(this.lastCast.x, this.lastCast.y, event.x, event.y) / 100;
        const hasRuleOfLaw = this.hasBuff(SPELL_ID_RULE_OF_LAW);
        // We calculate the mastery effectiveness of this *one* heal
        const masteryEffectiveness = CombatLogParser.calculateMasteryEffectiveness(distance, hasRuleOfLaw);

        // The base healing of the spell (excluding any healing added by mastery)
        const baseHealingDone = healingDone / (1 + this.playerMasteryPerc * masteryEffectiveness);
        const masteryHealingDone = healingDone - baseHealingDone;
        // The max potential mastery healing if we had a mastery effectiveness of 100% on this spell. This does NOT include the base healing
        // Example: a heal that did 1,324 healing with 32.4% mastery with 100% mastery effectiveness will have a max potential mastery healing of 324.
        const maxPotentialMasteryHealing = baseHealingDone * this.playerMasteryPerc; // * 100% mastery effectiveness

        this.masteryHealEvents.push({
          ...event,
          distance,
          masteryEffectiveness,
          baseHealingDone,
          masteryHealingDone,
          maxPotentialMasteryHealing,
        });
      }
      this.totalHealing += healingDone;
    }
  }
  parse_combatantinfo(event) {
    console.log('combatantinfo', event);

    this.players[event.sourceID] = event;

    if (this.byPlayer(event)) {
      this.playerMasteryPerc = CombatLogParser.calculateMasteryPercentage(event.mastery);

      event.auras.forEach(aura => {
        this.applyActiveBuff({
          ability: {
            abilityIcon: aura.icon,
            guid: aura.ability,
          },
          sourceID: aura.source,
        });
      });
    }
  }
  parse_absorbed(event) {
    if (this.byPlayer(event)) {
      this.totalHealing += event.amount;
    }
  }

  byPlayer(event) {
    return (event.sourceID === this.player.id);
  }
  toPlayer(event) {
    return (event.targetID === this.player.id);
  }
  playerEvent(event) {
    if (!event.x || !event.y) {
      return;
    }
    this.verifyCast(event);
    this.lastCast = event;
  }
  verifyCast(event) {
    if (!event.x || !event.y || !this.lastCast) {
      return;
    }
    const distance = CombatLogParser.calculateDistance(this.lastCast.x, this.lastCast.y, event.x, event.y) / 100;
    const timeSince = event.timestamp - this.lastCast.timestamp;
    const maxDistance = Math.max(1, timeSince / 1000 * 10 * 1.1); // 10 yards per second + 10% margin of error
    if (distance > maxDistance) {
      console.warn('Distance since previous event (' + (Math.round(timeSince / 100) / 10) + 's ago) was ' + (Math.round(distance * 10) / 10) + ' yards:', event.type, event, this.lastCast.type, this.lastCast);
    }
  }

  // region Buffs

  /**
   * Contains all buffs which are active right now.
   * @type {Array}
   */
  activeBuffs = [];
  /**
   * Keeps track of all buffs that have been applied during the fight.
   * @type {Array}
   */
  buffHistory = [];

  hasBuff(buffAbilityId) {
    return this.activeBuffs.find(buff => buff.ability.guid === buffAbilityId) !== undefined;
  }
  getBuffUptime(buffAbilityId) {
    return this.buffHistory.reduce((uptime, buff) => {
      if (buff.ability.guid === buffAbilityId) {
        uptime += buff.uptime;
      }
      return uptime;
    }, 0);
  }

  applyActiveBuff(buff) {
    this.activeBuffs.push(buff);
  }
  removeActiveBuff(activeBuff) {
    const activeBuffIndex = this.activeBuffs.findIndex(buff => buff.ability.guid === activeBuff.ability.guid);
    let appliedAtTimestamp = null;
    if (activeBuffIndex !== -1) {
      const removedBuff = this.activeBuffs.splice(activeBuffIndex, 1)[0];
      appliedAtTimestamp = removedBuff.timestamp;
    } else {
      console.error('Tried to remove buff', activeBuff, ', but couldn\'t find it in activeBuffs.');
      appliedAtTimestamp = this.fight.start_time;
    }

    this.buffHistory.push({
      ...activeBuff,
      uptime: activeBuff.timestamp - appliedAtTimestamp,
    });
  }

  parse_applybuff(event) {
    if (!this.toPlayer(event)) return;

    this.applyActiveBuff(event);
  }
  parse_refreshbuff(event) {
    if (!this.toPlayer(event)) return;

    this.removeActiveBuff(event);
    this.applyActiveBuff(event);
  }
  parse_removebuff(event) {
    if (!this.toPlayer(event)) return;

    this.removeActiveBuff(event);
  }

  // endregion

  static calculateDistance(x1, y1, x2, y2) {
    return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
  }
  static calculateMasteryEffectiveness(distance, hasRuleOfLaw) {
    const fullEffectivenessRadius = hasRuleOfLaw ? 15 : 10;
    const falloffRadius = hasRuleOfLaw ? 60 : 40;

    return Math.min(1, Math.max(0, 1 - (distance - fullEffectivenessRadius) / (falloffRadius - fullEffectivenessRadius)));
  }
  static calculateMasteryPercentage(masteryRating) {
    return 0.12 + masteryRating / 26667;
  }
}

export default CombatLogParser;
<|endoftext|>


<commit_before>'use strict';

// #############################################################################
// Drag'n'drop plugins

var globals = require('./settings/globals');
var casperjs = require('casper');
var cms = require('./helpers/cms')(casperjs);

casper.test.setUp(function (done) {
    casper.start()
        .then(cms.login())
        .then(cms.addPage({ title: 'First page' }))
        // actually creates 3 plugins - row > col + col
        .then(cms.addPlugin({
            type: 'GridPlugin',
            content: {
                id_create: 2,
                id_create_size: 12
            }
        }))
        .then(cms.addPlugin({
            type: 'TextPlugin',
            content: {
                id_body: 'Test text'
            }
        }))
        .run(done);
});

casper.test.tearDown(function (done) {
    casper.start()
        .then(cms.removePage())
        .then(cms.logout())
        .run(done);
});

casper.test.begin('Drag plugin to another placeholder', function (test) {
    casper.start(globals.editUrl)
        .then(function () {
            test.assertElementCount('.cms-structure .cms-draggable', 4);
        })
        // make sure we are in structure mode
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .waitUntilVisible('.cms-structure')
        // check that the plugins are in correct placeholders
        .then(function () {
            test.assertElementCount(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                2,
                'Both plugins are in first placeholder'
            );
            test.assertElementCount(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                0,
                'Plugin is not yet moved to another placeholder'
            );
        })
        // move plugin
        .then(function () {
            var placeholder = this.getElementBounds('.cms-dragarea:nth-child(2) .cms-draggables');

            this.evaluate(function () {
                // changing delay here because casper doesn't understand
                // that we want to wait 100 ms after "picking up" the plugin
                // for sortable to work
                CMS.API.StructureBoard.ui.sortables.nestedSortable('option', 'delay', 0);
            });

            this.mouse.down('.cms-dragarea:first-child > .cms-draggables > .cms-draggable');
            this.mouse.move(placeholder.left + placeholder.width / 2, placeholder.top + placeholder.height * 0);
        }).then(function () {
            this.mouse.up('.cms-dragarea:nth-child(2) .cms-draggables');
            test.assertElementCount(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                1,
                'Plugin moved to another placeholder'
            );
        })
        .waitForResource(/move-plugin/)
        .then(function () {
            return this.reload();
        })
        // check after reload
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .then(function () {
            test.assertElementCount(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                1,
                'One plugin is in the first placeholder'
            );
            test.assertSelectorHasText(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                'Text',
                'First placeholder has Text Plugin'
            );
            test.assertElementCount(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                1,
                'Another plugin is in the second placeholder'
            );
            test.assertSelectorHasText(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                'Multi Columns (grid)',
                'Second placeholder has MultiColumns Plugin'
            );
        })
        .run(function () {
            test.done();
        });
});

casper.test.begin('Move plugins inside a plugin', function (test) {
    var drop;

    casper.start(globals.editUrl)
        .then(function () {
            test.assertElementCount('.cms-structure .cms-draggable', 4);
        })
        // make sure we are in structure mode
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .waitUntilVisible('.cms-structure')
        // expand the multi columns plugin
        .thenBypassIf(function () {
            // if its already expanded, skip
            return this.visible('.cms-dragitem-text[title*="GridColumnPlugin"]');
        }, 2)
        .then(function () {
            this.mouse.click(
                '.cms-dragarea:first-child > .cms-draggables > .cms-draggable > .cms-dragitem-collapsable'
            );
        })
        .then(function () {
            // have to wait a bit because the collapse is debounced
            this.wait(10);
            test.assertVisible('.cms-dragitem-text[title*="GridColumnPlugin"]');
        })
        // move plugin
        .then(function () {
            // first grid column
            drop = this.getElementBounds(
                '.cms-dragarea:first-child > .cms-draggables > .cms-draggable .cms-draggable:first-child');

            this.evaluate(function () {
                // changing delay here because casper doesn't understand
                // that we want to wait 100 ms after "picking up" the plugin
                // for sortable to work
                CMS.API.StructureBoard.ui.sortables.nestedSortable('option', 'delay', 0);
            });

            this.mouse.down('.cms-dragarea:first-child > .cms-draggables > .cms-draggable:last-child');
            this.mouse.move(drop.left + drop.width / 2, drop.top + drop.height);
            this.mouse.move(drop.left + drop.width / 2 + 30, drop.top + drop.height);
        }).then(function () {
            this.mouse.up(drop.left + drop.width / 2 + 30, drop.top + drop.height);
        })
        .waitForResource(/move-plugin/)
        .then(function () {
            this.reload();
        })
        // check after reload
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .then(function () {
            test.assertElementCount(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                1,
                'There is only one root plugin'
            );

            test.assertEvalEquals(function () {
                return $('.cms-dragitem-text[title*=GridColumnPlugin]:first')
                    .closest('.cms-dragitem')
                    .hasClass('cms-dragitem-collapsable');
            }, true, 'First grid column has a child');

            test.assertEvalEquals(function () {
                return $('.cms-dragitem-text[title*=GridColumnPlugin]:first')
                    .closest('.cms-draggable')
                    .find('.cms-draggables')
                    .find('.cms-dragitem-text strong').text() === 'Text';
            }, true, 'First grid column has TextPlugin as a child');
        })
        // collapse the multi columns plugin
        .then(function () {
            this.mouse.click(
                '.cms-dragarea:first-child > .cms-draggables > .cms-draggable > .cms-dragitem-collapsable'
            );
        })
        .run(function () {
            test.done();
        });
});
<commit_msg>attempt to fix travis build<commit_after>'use strict';

// #############################################################################
// Drag'n'drop plugins

var globals = require('./settings/globals');
var casperjs = require('casper');
var cms = require('./helpers/cms')(casperjs);

casper.test.setUp(function (done) {
    casper.start()
        .then(cms.login())
        .then(cms.addPage({ title: 'First page' }))
        // actually creates 3 plugins - row > col + col
        .then(cms.addPlugin({
            type: 'GridPlugin',
            content: {
                id_create: 2,
                id_create_size: 12
            }
        }))
        .then(cms.addPlugin({
            type: 'TextPlugin',
            content: {
                id_body: 'Test text'
            }
        }))
        .run(done);
});

casper.test.tearDown(function (done) {
    casper.start()
        .then(cms.removePage())
        .then(cms.logout())
        .run(done);
});

casper.test.begin('Drag plugin to another placeholder', function (test) {
    casper.start(globals.editUrl)
        .then(function () {
            test.assertElementCount('.cms-structure .cms-draggable', 4);
        })
        // make sure we are in structure mode
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .waitUntilVisible('.cms-structure')
        // check that the plugins are in correct placeholders
        .then(function () {
            test.assertElementCount(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                2,
                'Both plugins are in first placeholder'
            );
            test.assertElementCount(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                0,
                'Plugin is not yet moved to another placeholder'
            );
        })
        // move plugin
        .then(function () {
            var placeholder = this.getElementBounds('.cms-dragarea:nth-child(2) .cms-draggables');

            this.evaluate(function () {
                // changing delay here because casper doesn't understand
                // that we want to wait 100 ms after "picking up" the plugin
                // for sortable to work
                CMS.API.StructureBoard.ui.sortables.nestedSortable('option', 'delay', 0);
            });

            this.mouse.down('.cms-dragarea:first-child > .cms-draggables > .cms-draggable');
            this.mouse.move(placeholder.left + placeholder.width / 2, placeholder.top + placeholder.height * 0);
        }).then(function () {
            this.mouse.up('.cms-dragarea:nth-child(2) .cms-draggables');
            test.assertElementCount(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                1,
                'Plugin moved to another placeholder'
            );
        })
        .waitForResource(/move-plugin/)
        .then(function () {
            return this.reload();
        })
        // check after reload
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .then(function () {
            test.assertElementCount(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                1,
                'One plugin is in the first placeholder'
            );
            test.assertSelectorHasText(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                'Text',
                'First placeholder has Text Plugin'
            );
            test.assertElementCount(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                1,
                'Another plugin is in the second placeholder'
            );
            test.assertSelectorHasText(
                '.cms-dragarea:nth-child(2) > .cms-draggables > .cms-draggable',
                'Multi Columns (grid)',
                'Second placeholder has MultiColumns Plugin'
            );
        })
        .run(function () {
            test.done();
        });
});

casper.test.begin('Move plugins inside a plugin', function (test) {
    var drop;

    casper.start(globals.editUrl)
        .then(function () {
            test.assertElementCount('.cms-structure .cms-draggable', 4);
        })
        // make sure we are in structure mode
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .waitUntilVisible('.cms-structure')
        // expand the multi columns plugin
        .thenBypassIf(function () {
            // if its already expanded, skip
            return this.visible('.cms-dragitem-text[title*="GridColumnPlugin"]');
        }, 2)
        .then(function () {
            this.mouse.click(
                '.cms-dragarea:first-child > .cms-draggables > .cms-draggable > .cms-dragitem-collapsable'
            );
        })
        // have to wait a bit because the collapse is debounced
        .wait(100)
        .then(function () {
            test.assertVisible('.cms-dragitem-text[title*="GridColumnPlugin"]', 'Plugin expanded');
        })
        // move plugin
        .then(function () {
            // first grid column
            drop = this.getElementBounds(
                '.cms-dragarea:first-child > .cms-draggables > .cms-draggable .cms-draggable:first-child');

            this.evaluate(function () {
                // changing delay here because casper doesn't understand
                // that we want to wait 100 ms after "picking up" the plugin
                // for sortable to work
                CMS.API.StructureBoard.ui.sortables.nestedSortable('option', 'delay', 0);
            });

            this.mouse.down('.cms-dragarea:first-child > .cms-draggables > .cms-draggable:last-child');
            this.mouse.move(drop.left + drop.width / 2, drop.top + drop.height);
            this.mouse.move(drop.left + drop.width / 2 + 30, drop.top + drop.height);
        }).then(function () {
            this.mouse.up(drop.left + drop.width / 2 + 30, drop.top + drop.height);
        })
        .waitForResource(/move-plugin/)
        .then(function () {
            this.reload();
        })
        // check after reload
        .waitUntilVisible('.cms-toolbar-expanded', function () {
            this.click('.cms-toolbar-item-cms-mode-switcher .cms-btn[href="?build"]');
        })
        .then(function () {
            test.assertElementCount(
                '.cms-dragarea:nth-child(1) > .cms-draggables > .cms-draggable',
                1,
                'There is only one root plugin'
            );

            test.assertEvalEquals(function () {
                return $('.cms-dragitem-text[title*=GridColumnPlugin]:first')
                    .closest('.cms-dragitem')
                    .hasClass('cms-dragitem-collapsable');
            }, true, 'First grid column has a child');

            test.assertEvalEquals(function () {
                return $('.cms-dragitem-text[title*=GridColumnPlugin]:first')
                    .closest('.cms-draggable')
                    .find('.cms-draggables')
                    .find('.cms-dragitem-text strong').text() === 'Text';
            }, true, 'First grid column has TextPlugin as a child');
        })
        // collapse the multi columns plugin
        .then(function () {
            this.mouse.click(
                '.cms-dragarea:first-child > .cms-draggables > .cms-draggable > .cms-dragitem-collapsable'
            );
        })
        .run(function () {
            test.done();
        });
});
<|endoftext|>


<commit_before>define(function(require) {

  var _ = require('underscore');
  var $ = require('jquery');
  var Backbone = require('backbone');
  var utils = require('utils');
  require('autosize');

  var EditorView = Backbone.View.extend({

    el: '#editor',

    initialize: function(options) {

      _.bindAll(this, 'render', 'focus', 'blur', 'desktopFocus', 'handleCyrillic');

      this.model.on('change:id', this.render);
      this.model.on('change:content', this.handleCyrillic);
      this.model.on('update', this.render);

      this.$el.autosize();
    },

    // only re-render when content changed
    render: function() {
      var content = this.model.get('content');
      if ( content === this.$el.val() ) return;
      // strip html tags from documents.
      // this is just for migration from contenteditable to textarea.
      // we can remove this later on.
      content = content.replace(/
/ig,'\n').replace(/<[^>]+>/ig,'');
      this.$el.val(content || '').trigger('autosize.resize');
    },

    focus: function() {
      if (utils.isDesktop) return this.$el.focus();
      setTimeout(_.bind(this.$el.focus, this.$el), 500);
    },

    desktopFocus: function() {
      if (utils.isDesktop) this.focus();
    },

    blur: function() {
      this.$el.blur();
    },

    events: {
      'keyup': 'updateOpenDoc'
    },

    updateOpenDoc: function(e) {
      this.model.set( 'content', this.$el.val() );
    },

    // if a cyrillic doc is detected change to cyrillic font
    handleCyrillic: function(doc) {
      // see http://kourge.net/projects/regexp-unicode-block
      var isCyrillic = doc.get('content').match('[\u0400-\u04FF\u0500-\u052F]');
      if (!isCyrillic) return;
      this.$el.addClass('cyrillic');
      this.model.off(null, this.handleCyrillic);
    }

  });

  return EditorView;
});
<commit_msg>focus editor when switching docs on desktop<commit_after>define(function(require) {

  var _ = require('underscore');
  var $ = require('jquery');
  var Backbone = require('backbone');
  var utils = require('utils');
  require('autosize');

  var EditorView = Backbone.View.extend({

    el: '#editor',

    initialize: function(options) {

      _.bindAll(this, 'render', 'focus', 'blur', 'desktopFocus', 'handleCyrillic');

      this.model.on('change:id', this.render);
      this.model.on('change:content', this.handleCyrillic);
      this.model.on('update', this.render);

      this.$el.autosize();
    },

    // only re-render when content changed
    render: function() {
      var content = this.model.get('content');
      if ( content === this.$el.val() ) return;
      // strip html tags from documents.
      // this is just for migration from contenteditable to textarea.
      // we can remove this later on.
      content = content.replace(/
/ig,'\n').replace(/<[^>]+>/ig,'');
      this.$el.val(content || '').trigger('autosize.resize');
      this.desktopFocus();
    },

    focus: function() {
      if (utils.isDesktop) return this.$el.focus();
      setTimeout(_.bind(this.$el.focus, this.$el), 500);
    },

    desktopFocus: function() {
      if (utils.isDesktop) this.focus();
    },

    blur: function() {
      this.$el.blur();
    },

    events: {
      'keyup': 'updateOpenDoc'
    },

    updateOpenDoc: function(e) {
      this.model.set( 'content', this.$el.val() );
    },

    // if a cyrillic doc is detected change to cyrillic font
    handleCyrillic: function(doc) {
      // see http://kourge.net/projects/regexp-unicode-block
      var isCyrillic = doc.get('content').match('[\u0400-\u04FF\u0500-\u052F]');
      if (!isCyrillic) return;
      this.$el.addClass('cyrillic');
      this.model.off(null, this.handleCyrillic);
    }

  });

  return EditorView;
});
<|endoftext|>


<commit_before>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { Fixture } from '../../../karma';
import { useStory } from '../../../app/story';
import { BORDER_POSITION } from '../../../constants';

describe('Border Panel', () => {
  let fixture;

  beforeEach(async () => {
    fixture = new Fixture();
    await fixture.render();
  });

  afterEach(() => {
    fixture.restore();
  });

  const getSelection = async () => {
    const storyContext = await fixture.renderHook(() => useStory());
    return storyContext.state.selectedElements;
  };

  describe('CUJ: Creator can Manipulate Shape', () => {
    it('should allow the user to add border for text element', async () => {
      await fixture.events.click(fixture.editor.library.textAdd);
      const panel = fixture.editor.inspector.designPanel.border;

      await fixture.events.click(panel.width('Left'), { clickCount: 3 });
      await fixture.events.keyboard.type('2');
      await fixture.events.keyboard.press('tab');

      let [element] = await getSelection();
      // Since the default state is locked, verify that all the values were set to 10.
      expect(element.border.left).toBe(2);
      expect(element.border.top).toBe(2);
      expect(element.border.right).toBe(2);
      expect(element.border.bottom).toBe(2);

      // Take off lock.
      await fixture.events.click(panel.lockBorderWidth.button);

      await fixture.events.click(panel.width('Top'), { clickCount: 3 });
      await fixture.events.keyboard.type('10');
      await fixture.events.keyboard.press('tab');

      // Verify the value changed but stayed the same for the left width.
      [element] = await getSelection();
      expect(element.border.left).toBe(2);
      expect(element.border.top).toBe(10);

      await fixture.snapshot('Text element with inner border');
    });

    it('should allow user to add border for media', async () => {
      // Add media element and basic border.
      await fixture.events.click(fixture.editor.library.media.item(0));
      const panel = fixture.editor.inspector.designPanel.border;
      await fixture.events.click(panel.width('Left'), { clickCount: 3 });
      await fixture.events.keyboard.type('10');
      await fixture.events.keyboard.press('Tab');

      await fixture.events.click(panel.position('Outside').button);

      const borderColor = panel.borderColor;
      await fixture.events.click(borderColor.opacity, { clickCount: 3 });
      await fixture.events.keyboard.type('30');
      await fixture.events.keyboard.press('Tab');

      const [element] = await getSelection();
      const {
        border: { left, top, right, bottom, color, position },
      } = element;
      expect(left).toBe(10);
      expect(top).toBe(10);
      expect(right).toBe(10);
      expect(bottom).toBe(10);
      expect(position).toBe(BORDER_POSITION.OUTSIDE);
      expect(color.color.a).toBe(0.3);

      await fixture.snapshot('Media element with outside border');
    });
  });

  it('should allow user to add border for shape', async () => {
    await fixture.events.click(fixture.editor.library.shapesTab);
    await fixture.events.click(
      fixture.editor.library.shapes.shape('Rectangle')
    );

    const panel = fixture.editor.inspector.designPanel.border;
    await fixture.events.click(panel.width('Right'), { clickCount: 3 });
    await fixture.events.keyboard.type('5');
    await fixture.events.keyboard.press('Tab');

    await fixture.events.click(panel.position('Center').button);

    const [element] = await getSelection();
    const {
      border: { position },
    } = element;
    expect(position).toBe(BORDER_POSITION.CENTER);

    await fixture.snapshot('Shape element with center border');
  });

  it('should not allow border for non-rectangular shape', async () => {
    await fixture.events.click(fixture.editor.library.shapesTab);
    await fixture.events.click(fixture.editor.library.shapes.shape('Circle'));
    // Verify that panel is not found.
    expect(() => fixture.editor.inspector.designPanel.border).toThrow();
  });
});
<commit_msg>Karma update.<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { Fixture } from '../../../karma';
import { useStory } from '../../../app/story';
import { BORDER_POSITION } from '../../../constants';

describe('Border Panel', () => {
  let fixture;

  beforeEach(async () => {
    fixture = new Fixture();
    await fixture.render();
  });

  afterEach(() => {
    fixture.restore();
  });

  const getSelection = async () => {
    const storyContext = await fixture.renderHook(() => useStory());
    return storyContext.state.selectedElements;
  };

  describe('CUJ: Creator can Manipulate Shape', () => {
    it('should allow the user to add border for text element', async () => {
      await fixture.events.click(fixture.editor.library.textAdd);
      const panel = fixture.editor.inspector.designPanel.border;

      await fixture.events.click(panel.width('Left'), { clickCount: 3 });
      await fixture.events.keyboard.type('2');
      await fixture.events.keyboard.press('tab');

      const [element] = await getSelection();
      // Since the default state is locked, verify that all the values were set to 10.
      expect(element.border.left).toBe(2);
      expect(element.border.top).toBe(2);
      expect(element.border.right).toBe(2);
      expect(element.border.bottom).toBe(2);

      // Take off lock.
      await fixture.events.click(panel.lockBorderWidth.button);

      await fixture.events.click(panel.width('Top'), { clickCount: 3 });
      await fixture.events.keyboard.type('10');
      await fixture.events.keyboard.press('tab');

      // Verify the value changed but stayed the same for the left width.
      const [updatedElement] = await getSelection();
      expect(updatedElement.border.left).toBe(2);
      expect(updatedElement.border.top).toBe(10);

      await fixture.snapshot('Text element with inner border');
    });

    it('should allow user to add border for media', async () => {
      // Add media element and basic border.
      await fixture.events.click(fixture.editor.library.media.item(0));
      const panel = fixture.editor.inspector.designPanel.border;
      await fixture.events.click(panel.width('Left'), { clickCount: 3 });
      await fixture.events.keyboard.type('10');
      await fixture.events.keyboard.press('Tab');

      await fixture.events.click(panel.position('Outside').button);

      const borderColor = panel.borderColor;
      await fixture.events.click(borderColor.opacity, { clickCount: 3 });
      await fixture.events.keyboard.type('30');
      await fixture.events.keyboard.press('Tab');

      const [element] = await getSelection();
      const {
        border: { left, top, right, bottom, color, position },
      } = element;
      expect(left).toBe(10);
      expect(top).toBe(10);
      expect(right).toBe(10);
      expect(bottom).toBe(10);
      expect(position).toBe(BORDER_POSITION.OUTSIDE);
      expect(color.color.a).toBe(0.3);

      await fixture.snapshot('Media element with outside border');
    });
  });

  it('should allow user to add border for shape', async () => {
    await fixture.events.click(fixture.editor.library.shapesTab);
    await fixture.events.click(
      fixture.editor.library.shapes.shape('Rectangle')
    );

    const panel = fixture.editor.inspector.designPanel.border;
    await fixture.events.click(panel.width('Right'), { clickCount: 3 });
    await fixture.events.keyboard.type('5');
    await fixture.events.keyboard.press('Tab');

    await fixture.events.click(panel.position('Center').button);

    const [element] = await getSelection();
    const {
      border: { position },
    } = element;
    expect(position).toBe(BORDER_POSITION.CENTER);

    await fixture.snapshot('Shape element with center border');
  });

  it('should not allow border for non-rectangular shape', async () => {
    await fixture.events.click(fixture.editor.library.shapesTab);
    await fixture.events.click(fixture.editor.library.shapes.shape('Circle'));
    // Verify that panel is not found.
    expect(() => fixture.editor.inspector.designPanel.border).toThrow();
  });
});
<|endoftext|>