<commit_before>module("About Functions And Closure (topics/about_functions_and_closure.js)");

test("defining functions directly", function() {
    var result = "a";
    function changeResult() {
        // the ability to access a variables defined in the same scope as the function is known as 'closure'
        result = "b";
    };
    changeResult();
    equal(__, result, 'what is the value of result?');
});

test("assigning functions to variables", function() {
    var triple = function(input) {
        return input * 3;
    };
    equal(__, triple(4), 'what is triple 4?');
});

test("self invoking functions", function() {
    var publicValue = "shared";

    // self invoking functions are used to provide scoping and to alias variables
    (function(pv) {
        var secretValue = "password";
        equal(__, pv, 'what is the value of pv?');
        equal("__", typeof(secretValue), "is secretValue available in this context?");
        equal("__", typeof(publicValue), "is publicValue available in this context?");
    })(publicValue);

    equal("__", typeof(secretValue), "is secretValue available in this context?");
    equal("__", typeof(publicValue), "is publicValue available in this context?");
});

test("arguments array", function() {
    var add = function() {
        var total = 0;
        for(var i = 0; i < arguments.length; i++) {
            // complete the implementation of this method so that it returns the sum of its arguments
            // __
        }
        // __
    };

    equal(15, add(1,2,3,4,5), "add 1,2,3,4,5");
    equal(9, add(4,7,-2), "add 4,7,-2");
});

test("using call to invoke function",function(){
    var invokee = function( message ){
        return this + message;
    };

    //another way to invoke a function is to use the call function which allows
    //you to set the callers "this" context.  Call can take any number of arguments:
    //the first one is always the context that this should be set to in the called
    //function, and the arguments to be sent to the function,multiple arguments are separated by commas.
    var result = invokee.call("I am this!", "Where did it come from?");

    equal(__, result, "what will the value of invokee's this be?");
});

test("using apply to invoke function",function(){
    var invokee = function( message1, message2 ){
        return this + message1 + message2;
    };

    //similar to the call function is the apply function.  Apply only has two
    //arguments:  the first is the context that this should be set to in the called
    //function and and array of arguments to be passed into the called function.
    var result = invokee.apply("I am this!", ["I am arg1","I am arg2"]);

    equal(__, result, "what will the value of invokee's this be?");
});

<commit_msg>Complete about_functions_and_closure<commit_after>module("About Functions And Closure (topics/about_functions_and_closure.js)");

test("defining functions directly", function() {
    var result = "a";
    function changeResult() {
        // the ability to access a variables defined in the same scope as the function is known as 'closure'
        result = "b";
    };
    changeResult();
    equal("b", result, 'what is the value of result?');
});

test("assigning functions to variables", function() {
    var triple = function(input) {
        return input * 3;
    };
    equal(12, triple(4), 'what is triple 4?');
});

test("self invoking functions", function() {
    var publicValue = "shared";

    // self invoking functions are used to provide scoping and to alias variables
    (function(pv) {
        var secretValue = "password";
        equal("shared", pv, 'what is the value of pv?');
        equal("string", typeof(secretValue), "is secretValue available in this context?");
        equal("string", typeof(publicValue), "is publicValue available in this context?");
    })(publicValue);

    equal("undefined", typeof(secretValue), "is secretValue available in this context?");
    equal("string", typeof(publicValue), "is publicValue available in this context?");
});

test("arguments array", function() {
    var add = function() {
        var total = 0;
        for(var i = 0; i < arguments.length; i++) {
            // complete the implementation of this method so that it returns the sum of its arguments
            total += arguments[i]
        }
        return total
    };

    equal(15, add(1,2,3,4,5), "add 1,2,3,4,5");
    equal(9, add(4,7,-2), "add 4,7,-2");
});

test("using call to invoke function",function(){
    var invokee = function( message ){
        return this + message;
    };

    //another way to invoke a function is to use the call function which allows
    //you to set the callers "this" context.  Call can take any number of arguments:
    //the first one is always the context that this should be set to in the called
    //function, and the arguments to be sent to the function,multiple arguments are separated by commas.
    var result = invokee.call("I am this!", "Where did it come from?");

    equal("I am this!Where did it come from?", result, "what will the value of invokee's this be?");
});

test("using apply to invoke function",function(){
    var invokee = function( message1, message2 ){
        return this + message1 + message2;
    };

    //similar to the call function is the apply function.  Apply only has two
    //arguments:  the first is the context that this should be set to in the called
    //function and and array of arguments to be passed into the called function.
    var result = invokee.apply("I am this!", ["I am arg1","I am arg2"]);

    equal("I am this!I am arg1I am arg2", result, "what will the value of invokee's this be?");
});

<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import ClickListener from '../internal/ClickListener';
import FloatingMenu from '../internal/FloatingMenu';
import OptimizedResize from '../internal/OptimizedResize';
import Icon from './Icon';

class OverflowMenu extends Component {
  static propTypes = {
    open: PropTypes.bool,
    flipped: PropTypes.bool,
    floatingMenu: PropTypes.bool,
    children: PropTypes.node,
    className: PropTypes.string,
    tabIndex: PropTypes.number,
    id: PropTypes.string,
    ariaLabel: PropTypes.string,
    onClick: PropTypes.func,
    onFocus: PropTypes.func,
    onKeyDown: PropTypes.func,
    handleClick: PropTypes.func,
    iconDescription: PropTypes.string.isRequired,
    iconName: PropTypes.string,
    menuOffset: PropTypes.object,
    menuOffsetFlip: PropTypes.object,
    iconClass: PropTypes.string,
  };

  static defaultProps = {
    ariaLabel: 'list of options',
    iconDescription: 'open and close list of options',
    iconName: 'overflow-menu',
    open: false,
    flipped: false,
    floatingMenu: false,
    onClick: () => {},
    tabIndex: 0,
    menuOffset: { top: 0, left: 60.5 },
    menuOffsetFlip: { top: 0, left: -60.5 },
  };

  state = {
    open: this.props.open,
  };

  componentDidMount() {
    requestAnimationFrame(() => {
      this.getMenuPosition();
    });
    this.hResize = OptimizedResize.add(() => {
      this.getMenuPosition();
    });
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.open !== this.props.open) {
      this.setState({ open: nextProps.open });
    }
  }

  componentWillUnmount() {
    this.hResize.release();
  }

  getMenuPosition = () => {
    const menuPosition = this.menuEl.getBoundingClientRect();
    this.setState({ menuPosition });
  };

  handleClick = evt => {
    this.setState({ open: !this.state.open });
    this.props.onClick(evt);
  };

  handleKeyPress = evt => {
    const key = evt.key || evt.which;

    if (key === 'Enter' || key === 13 || key === ' ' || key === 32) {
      this.setState({ open: !this.state.open });
    }
  };

  handleClickOutside = () => {
    this.closeMenu();
  };

  closeMenu = () => {
    this.setState({ open: false });
  };

  render() {
    const {
      id,
      tabIndex,
      ariaLabel,
      children,
      iconDescription,
      iconName,
      flipped,
      floatingMenu,
      menuOffset,
      menuOffsetFlip,
      iconClass,
      onClick, // eslint-disable-line
      ...other
    } = this.props;

    const overflowMenuClasses = classNames(
      this.props.className,
      'bx--overflow-menu',
      {
        'bx--overflow-menu--open': this.state.open,
      }
    );

    const overflowMenuOptionsClasses = classNames('bx--overflow-menu-options', {
      'bx--overflow-menu--flip': this.props.flipped,
      'bx--overflow-menu-options--open': this.state.open,
    });

    const overflowMenuIconClasses = classNames(
      'bx--overflow-menu__icon',
      iconClass
    );

    const childrenWithProps = React.Children.toArray(children).map(child =>
      React.cloneElement(child, {
        closeMenu: this.closeMenu,
      })
    );

    return (
      <ClickListener onClickOutside={this.handleClickOutside}>

 {
            this.menuEl = node;
          }}
        >
          <Icon
            onClick={this.handleClick}
            className={overflowMenuIconClasses}
            name={iconName}
            description={iconDescription}
            style={{ width: '100%' }}
          />
          {floatingMenu
            ? <FloatingMenu
                menuPosition={this.state.menuPosition}
                menuDirection="bottom"
                menuOffset={flipped ? menuOffsetFlip : menuOffset}
              >

                  {childrenWithProps}

              </FloatingMenu>
            :

                {childrenWithProps}

}

      </ClickListener>
    );
  }
}

export default OverflowMenu;
<commit_msg>fix(overflow): only use menuEl if it exists (#160)<commit_after>import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import ClickListener from '../internal/ClickListener';
import FloatingMenu from '../internal/FloatingMenu';
import OptimizedResize from '../internal/OptimizedResize';
import Icon from './Icon';

class OverflowMenu extends Component {
  static propTypes = {
    open: PropTypes.bool,
    flipped: PropTypes.bool,
    floatingMenu: PropTypes.bool,
    children: PropTypes.node,
    className: PropTypes.string,
    tabIndex: PropTypes.number,
    id: PropTypes.string,
    ariaLabel: PropTypes.string,
    onClick: PropTypes.func,
    onFocus: PropTypes.func,
    onKeyDown: PropTypes.func,
    handleClick: PropTypes.func,
    iconDescription: PropTypes.string.isRequired,
    iconName: PropTypes.string,
    menuOffset: PropTypes.object,
    menuOffsetFlip: PropTypes.object,
    iconClass: PropTypes.string,
  };

  static defaultProps = {
    ariaLabel: 'list of options',
    iconDescription: 'open and close list of options',
    iconName: 'overflow-menu',
    open: false,
    flipped: false,
    floatingMenu: false,
    onClick: () => {},
    tabIndex: 0,
    menuOffset: { top: 0, left: 60.5 },
    menuOffsetFlip: { top: 0, left: -60.5 },
  };

  state = {
    open: this.props.open,
  };

  componentDidMount() {
    requestAnimationFrame(() => {
      this.getMenuPosition();
    });
    this.hResize = OptimizedResize.add(() => {
      this.getMenuPosition();
    });
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.open !== this.props.open) {
      this.setState({ open: nextProps.open });
    }
  }

  componentWillUnmount() {
    this.hResize.release();
  }

  getMenuPosition = () => {
    const menuPosition = this.menuEl.getBoundingClientRect();
    this.setState({ menuPosition });
  };

  handleClick = evt => {
    this.setState({ open: !this.state.open });
    this.props.onClick(evt);
  };

  handleKeyPress = evt => {
    const key = evt.key || evt.which;

    if (key === 'Enter' || key === 13 || key === ' ' || key === 32) {
      this.setState({ open: !this.state.open });
    }
  };

  handleClickOutside = () => {
    this.closeMenu();
  };

  closeMenu = () => {
    this.setState({ open: false });
  };

  bindMenuEl = menuEl => {
    this.menuEl = menuEl;
  };

  render() {
    const {
      id,
      tabIndex,
      ariaLabel,
      children,
      iconDescription,
      iconName,
      flipped,
      floatingMenu,
      menuOffset,
      menuOffsetFlip,
      iconClass,
      onClick, // eslint-disable-line
      ...other
    } = this.props;

    const overflowMenuClasses = classNames(
      this.props.className,
      'bx--overflow-menu',
      {
        'bx--overflow-menu--open': this.state.open,
      }
    );

    const overflowMenuOptionsClasses = classNames('bx--overflow-menu-options', {
      'bx--overflow-menu--flip': this.props.flipped,
      'bx--overflow-menu-options--open': this.state.open,
    });

    const overflowMenuIconClasses = classNames(
      'bx--overflow-menu__icon',
      iconClass
    );

    const childrenWithProps = React.Children.toArray(children).map(child =>
      React.cloneElement(child, {
        closeMenu: this.closeMenu,
      })
    );

    return (
      <ClickListener onClickOutside={this.handleClickOutside}>

          <Icon
            onClick={this.handleClick}
            className={overflowMenuIconClasses}
            name={iconName}
            description={iconDescription}
            style={{ width: '100%' }}
          />
          {floatingMenu
            ? <FloatingMenu
                menuPosition={this.state.menuPosition}
                menuDirection="bottom"
                menuOffset={flipped ? menuOffsetFlip : menuOffset}
              >

                  {childrenWithProps}

              </FloatingMenu>
            :

                {childrenWithProps}

}

      </ClickListener>
    );
  }
}

export default OverflowMenu;
<|endoftext|>


<commit_before>
var _ = require('lodash');
var deepEqual = require('deep-equal');

function StateApi() {
    // накапливаемый стэйт приложения
    this.state = {};
    // сохраняет изменения замещенные неявными, временными значениями
    this.implicitChanges = {};

    /**
     * implicitSet наследует поведение toggle но при этом,
     * этот toggle не должен изменять implicitChanges,
     * для этого вводим флажок.
     * @type {boolean}
     */
    this.implicitChangesLocked = false;
}

/**
 * Сбрасываем все состояние на заданное
 * @param {Object} [newState]
 * @param {Boolean} [runFinalizer=false]
 */
StateApi.prototype.assign = function(newState, runFinalizer) {
    this.state = newState || {};
    if (runFinalizer) {
        this.applyFinalizer();
    }
};

StateApi.prototype.clear = function() {
    this.assign(null, true);
};

StateApi.prototype.getState = function() {
    return this.state;
};

StateApi.prototype.defaults = function(newState) {
    return _.defaults(this.state, newState);
};

StateApi.prototype.get = function(name) {
    return this.state[name];
};

StateApi.prototype.del = function(name) {
    // да, это медленнее чем присвоение в undefined, но позволяет держать стэйт в чистоте не оставляя
    // мусорных ключей
    if (!this.implicitChangesLocked) delete this.implicitChanges[name];

    delete this.state[name];
};

StateApi.prototype.set = function(name, value) {
    if (!this.implicitChangesLocked) delete this.implicitChanges[name];

    this.state[name] = value;
};

StateApi.prototype.toggle = function(name, value) {
    if (value === void 0) {
        value = !this.state[name];
    }
    if (value) {
        this.set(name, value);
    } else {
        this.del(name);
    }
};

/**
 * Ставим неявное, временное значение,
 * запоминаем постоянное в implicitChanges
 * @param {String} name
 * @param {*} value
 */
StateApi.prototype.implicitSet = function(name, value) {
    this.implicitChanges[name] = this.state[name];
    this.implicitChangesLocked = true;
    this.toggle(name, value);
    this.implicitChangesLocked = false;
};

/**
 * Восстанавливаем постоянное значение из implicitChanges
 * @param {String} name
 */
StateApi.prototype.implicitRestore = function(name) {
    var value = this.implicitChanges[name];
    if (value === undefined) {
        delete this.state[name];
    } else {
        this.state[name] = this.implicitChanges[name];
    }
    delete this.implicitChanges[name];
};

/**
 * Возвращает результат сравнения текущего состояния и эмулированного через clone
 * без учета каких-либо параметров
 *
 * @param {StateApi} api
 * @param {String[]} ignoreParams - список параметров стэйта, которые не будем учитывать
 * @returns {boolean}
 */
StateApi.prototype.isEqual = function(api, ignoreParams) {
    var actualUri = omitParams(this),
        expectedUri = omitParams(api);

    function omitParams(state) {
        return _.omit(state.getState(), ignoreParams);
    }
    return deepEqual(expectedUri, actualUri);
};

/**
 * @returns {object} Стэйт который будет учавствовать в формировании урла
 */
StateApi.prototype.getShareState = function() {
    return this.state;
};

/**
 * Задает метод которым будет служить доводчиком состояния
 * @param {function} func функция с интерфейсом (state, diff)
 */
StateApi.prototype.setFinalizer = function(func) {
    this.finalizer = func;
};

/**
 * Применяет финализатор стэйта, если задан
 * @param {Object?} diff
 */
StateApi.prototype.applyFinalizer = function(diff) {
    if (this.finalizer) {
        this.finalizer(this, diff);
    }
    return this;
};

module.exports = StateApi;
<commit_msg>Remove implicit changes and finalizer<commit_after>
var _ = require('lodash');
var deepEqual = require('deep-equal');

function StateApi() {
    // накапливаемый стэйт приложения
    this.state = {};
}

/**
 * Сбрасываем все состояние на заданное
 * @param {Object} [newState]
 */
StateApi.prototype.assign = function(newState) {
    this.state = newState || {};
};

StateApi.prototype.clear = function() {
    this.assign(null, true);
};

StateApi.prototype.getState = function() {
    return this.state;
};

StateApi.prototype.defaults = function(newState) {
    return _.defaults(this.state, newState);
};

StateApi.prototype.get = function(name) {
    return this.state[name];
};

StateApi.prototype.del = function(name) {
    delete this.state[name];
};

StateApi.prototype.set = function(name, value) {
    this.state[name] = value;
};

StateApi.prototype.toggle = function(name, value) {
    if (value === void 0) {
        value = !this.state[name];
    }
    if (value) {
        this.set(name, value);
    } else {
        this.del(name);
    }
};

/**
 * Возвращает результат сравнения текущего состояния и эмулированного через clone
 * без учета каких-либо параметров
 *
 * @param {StateApi} api
 * @param {String[]} ignoreParams - список параметров стэйта, которые не будем учитывать
 * @returns {boolean}
 */
StateApi.prototype.isEqual = function(api, ignoreParams) {
    var actualUri = omitParams(this),
        expectedUri = omitParams(api);

    function omitParams(state) {
        return _.omit(state.getState(), ignoreParams);
    }
    return deepEqual(expectedUri, actualUri);
};

/**
 * @returns {object} Стэйт который будет учавствовать в формировании урла
 */
StateApi.prototype.getShareState = function() {
    return this.state;
};

module.exports = StateApi;
<|endoftext|>


<commit_before>goog.provide('lime.scheduleManager');

goog.require('goog.array');
goog.require('lime');

/**
 * Unified timer provider class
 * Don't create instances of this class. Used the shared instance.
 * @this lime.scheduleManager
 * @constructor
 */
lime.scheduleManager = new (function() {

    /**
     * Array of registered functions
     * @type {Array.<lime.scheduleManager.Task>}
     * @private
     */
    this.taskStack_ = [];

    /**
     * ScheduleManager is active
     * @type {Boolean}
     * @private
     */
    this.active_ = false;

    /**
     * Internal setInterval id
     * @type {number}
     * @private
     */
    this.intervalID_ = 0;

    /**
     * Maximum update rate in ms.
     * @type {number}
     * @private
     */
    this.displayRate_ = 1000 / 30;

    /**
     * Timer last fire timestamp
     * @type {number}
     * @private
     */
    this.lastRunTime_ = 0;

})();

/**
 * Scheduled task
 * @param {number} maxdelta Timer wait value after iteration.
 * @param {number} opt_limit Number of calls.
 * @constructor
 */
lime.scheduleManager.Task = function(maxdelta, opt_limit) {
    this.delta = this.maxdelta = maxdelta;
    this.limit = goog.isDef(opt_limit) ? opt_limit : -1;
    this.functionStack_ = [];
};

/**
 * Handle iteration
 * @param {number} dt Delta time since last iteration.
 * @private
 */
lime.scheduleManager.Task.prototype.step_ = function(dt) {
    if (!this.functionStack_.length) return;
    if (this.delta > dt) {
        this.delta -= dt;
    }
    else {
        var delta = this.maxdelta + dt - this.delta;
        this.delta = this.maxdelta - (dt - this.delta);
        if (this.delta < 0) this.delta = 0;
        var f;
        var i = this.functionStack_.length;
        while (--i >= 0) {
            f = this.functionStack_[i];
            if (f && f[0] && goog.isFunction(f[1]))
            (f[1]).call(f[2], delta);
        }
        if (this.limit != -1) {
            this.limit--;
            if (this.limit == 0) {
                lime.scheduleManager.unschedule(f[1], f[2]);
            }
        }
    }
};

lime.scheduleManager.taskStack_.push(new lime.scheduleManager.Task(0));

/**
 * Returns maximum fire rate in ms. If you need FPS then use 1000/x
 * @this {lime.scheduleManager}
 * @return {number} Display rate.
 */
lime.scheduleManager.getDisplayRate = function() {
    //todo: bad name
    return this.displayRate_;
};

/**
 * Sets maximum fire rate for the scheduler in ms.
 * If you have FPS then send 1000/x
 * @this {lime.scheduleManager}
 * @param {number} value New display rate.
 */
lime.scheduleManager.setDisplayRate = function(value) {
     this.displayRate_ = value;
     if (this.active_) {
         this.disable_();
         this.activate_();
     }
};

/**
 * Schedule a function. Passed function will be called on every frame
 * with delta time from last run time
 * @this {lime.ScheduleManager}
 * @param {function(number)} f Function to be called.
 * @param {object} context The context used when calling function.
 * @param {lime.scheduleManager.Task} opt_task Task object.
 */
lime.scheduleManager.schedule = function(f, context, opt_task) {
    var task = goog.isDef(opt_task) ? opt_task : this.taskStack_[0];
    goog.array.insert(task.functionStack_, [1, f, context]);
    goog.array.insert(this.taskStack_, task);
    if (!this.active_) {
        this.activate_();
    }
};

/**
 * Unschedule a function. For functions that have be previously scheduled
 * @this {lime.ScheduleManager}
 * @param {function(number)} f Function to be unscheduled.
 * @param {object} context Context used when scheduling.
 */
lime.scheduleManager.unschedule = function(f, context) {
    var j = this.taskStack_.length;
    while (--j >= 0) {
        var functionStack_ = this.taskStack_[j].functionStack_,
            fi, i = functionStack_.length;
        while (--i >= 0) {
            fi = functionStack_[i];
            if (fi[1] == f && fi[2] == context) {
                goog.array.remove(functionStack_, fi);

            }
        }
        if (functionStack_.length == 0 && j != 0) {
           goog.array.remove(this.taskStack_, functionStack_);
        }
    }
    // if no more functions: stop timers
    if (this.taskStack_.length == 1 &&
            this.taskStack_[0].functionStack_.length == 0) {
        this.disable_();
    }
};

/**
 * Start the internal timer functions
 * @this {lime.scheduleManager}
 * @private
 */
lime.scheduleManager.activate_ = function() {
    if (this.active_) return;
    // Todo: Firefox 4beta has a neat API window.mozRequestAnimationFrame()
    // This could possibly do better job in this case (but will ruin mockClock)
    // http://weblogs.mozillazine.org/roc/archives/2010/08/mozrequestanima.html

    this.lastRunTime_ = (new Date()).getTime();
    this.intervalID_ = setInterval(goog.bind(this.step_, this),
        this.getDisplayRate());
    this.active_ = true;
};

/**
 * Stop interval timer functions
 * @this {lime.scheduleManager}
 * @private
 */
lime.scheduleManager.disable_ = function() {
    if (!this.active_) return;

    clearInterval(this.intervalID_);
    this.active_ = false;
};

/**
 * Main step function that delegates to other objects waiting
 * @this {lime.scheduleManager}
 * @private
 */
lime.scheduleManager.step_ = function() {
    var t;
    var curTime = goog.now();
    var delta = curTime - this.lastRunTime_;
    if (delta < 0) delta = 1;
    var i = this.taskStack_.length;
    while (--i >= 0) {
        this.taskStack_[i].step_(delta);
    }
    this.lastRunTime_ = curTime;
};

/**
 * Change director's activity. Used for pausing updates when director is paused
 * @this {lime.scheduleManager}
 * @param {lime.Director} director Director.
 * @param {boolean} value Active or inactive?
 */
lime.scheduleManager.changeDirectorActivity = function(director, value) {
    var t, j = this.taskStack_.length;
    while (--j >= 0) {

    var t = this.taskStack_[j], f, context, d, i = t.functionStack_.length;
    while (--i >= 0) {
        f = t.functionStack_[i];
        context = f[2];
        if (goog.isFunction(context.getDirector)) {
            d = context.getDirector();
            if (d == director) {
                f[0] = value;
            }
        }
    }
    }
};

/**
 * Set up function to be called once after a delay
 * @param {function(number)} f Function to be called.
 * @param {object} context Context used when calling object.
 * @param {number} delay Delay before calling.
 */
lime.scheduleManager.callAfter = function(f, context, delay) {
    lime.scheduleManager.scheduleWithDelay(f, context, delay, 1);
};

/**
 * Set up function to be called repeatedly after a delay
 * @param {function(number)} f Function to be called.
 * @param {object} context Context used when calling object.
 * @param {number} delay Delay before calling.
 * @param {number} opt_limit Number of times to call.
 * @this {lime.scheduleManager}
 */
lime.scheduleManager.scheduleWithDelay = function(f, context,
        delay, opt_limit) {
    var task = new lime.scheduleManager.Task(delay, opt_limit);
    this.schedule(f, context, task);
};
<commit_msg>RequestAnimationFrame support for scheduleManager for smoother animations. No more timer events needed. Currently supported in Firefox 4 and Chrome Canary.<commit_after>goog.provide('lime.scheduleManager');

goog.require('goog.array');
goog.require('goog.userAgent');
goog.require('lime');

/**
 * Unified timer provider class
 * Don't create instances of this class. Used the shared instance.
 * @this lime.scheduleManager
 * @constructor
 */
lime.scheduleManager = new (function() {

    /**
     * Array of registered functions
     * @type {Array.<lime.scheduleManager.Task>}
     * @private
     */
    this.taskStack_ = [];

    /**
     * ScheduleManager is active
     * @type {Boolean}
     * @private
     */
    this.active_ = false;

    /**
     * Internal setInterval id
     * @type {number}
     * @private
     */
    this.intervalID_ = 0;

    /**
     * Maximum update rate in ms.
     * @type {number}
     * @private
     */
    this.displayRate_ = 1000 / 30;

    /**
     * Timer last fire timestamp
     * @type {number}
     * @private
     */
    this.lastRunTime_ = 0;

})();

/**
 * Scheduled task
 * @param {number} maxdelta Timer wait value after iteration.
 * @param {number} opt_limit Number of calls.
 * @constructor
 */
lime.scheduleManager.Task = function(maxdelta, opt_limit) {
    this.delta = this.maxdelta = maxdelta;
    this.limit = goog.isDef(opt_limit) ? opt_limit : -1;
    this.functionStack_ = [];
};

/**
 * Handle iteration
 * @param {number} dt Delta time since last iteration.
 * @private
 */
lime.scheduleManager.Task.prototype.step_ = function(dt) {
    if (!this.functionStack_.length) return;
    if (this.delta > dt) {
        this.delta -= dt;
    }
    else {
        var delta = this.maxdelta + dt - this.delta;
        this.delta = this.maxdelta - (dt - this.delta);
        if (this.delta < 0) this.delta = 0;
        var f;
        var i = this.functionStack_.length;
        while (--i >= 0) {
            f = this.functionStack_[i];
            if (f && f[0] && goog.isFunction(f[1]))
            (f[1]).call(f[2], delta);
        }
        if (this.limit != -1) {
            this.limit--;
            if (this.limit == 0) {
                lime.scheduleManager.unschedule(f[1], f[2]);
            }
        }
    }
};

lime.scheduleManager.taskStack_.push(new lime.scheduleManager.Task(0));

/**
 * Whether to use requestAnimationFrame instead of timer events
 * Exposed here so it could be disabled if needed.
 * @type {boolean}
 */
lime.scheduleManager.USE_ANIMATION_FRAME = goog.global['mozRequestAnimationFrame'] ||
    goog.global['webkitRequestAnimationFrame'];

/**
 * Returns maximum fire rate in ms. If you need FPS then use 1000/x
 * @this {lime.scheduleManager}
 * @return {number} Display rate.
 */
lime.scheduleManager.getDisplayRate = function() {
    //todo: bad name
    return this.displayRate_;
};

/**
 * Sets maximum fire rate for the scheduler in ms.
 * If you have FPS then send 1000/x
 * Note that if animation frame methods are used browser chooses
 * max display rate and this value has no effect.
 * @this {lime.scheduleManager}
 * @param {number} value New display rate.
 */
lime.scheduleManager.setDisplayRate = function(value) {
     this.displayRate_ = value;
     if (this.active_) {
         this.disable_();
         this.activate_();
     }
};

/**
 * Schedule a function. Passed function will be called on every frame
 * with delta time from last run time
 * @this {lime.ScheduleManager}
 * @param {function(number)} f Function to be called.
 * @param {object} context The context used when calling function.
 * @param {lime.scheduleManager.Task} opt_task Task object.
 */
lime.scheduleManager.schedule = function(f, context, opt_task) {
    var task = goog.isDef(opt_task) ? opt_task : this.taskStack_[0];
    goog.array.insert(task.functionStack_, [1, f, context]);
    goog.array.insert(this.taskStack_, task);
    if (!this.active_) {
        this.activate_();
    }
};

/**
 * Unschedule a function. For functions that have be previously scheduled
 * @this {lime.ScheduleManager}
 * @param {function(number)} f Function to be unscheduled.
 * @param {object} context Context used when scheduling.
 */
lime.scheduleManager.unschedule = function(f, context) {
    var j = this.taskStack_.length;
    while (--j >= 0) {
        var functionStack_ = this.taskStack_[j].functionStack_,
            fi, i = functionStack_.length;
        while (--i >= 0) {
            fi = functionStack_[i];
            if (fi[1] == f && fi[2] == context) {
                goog.array.remove(functionStack_, fi);

            }
        }
        if (functionStack_.length == 0 && j != 0) {
           goog.array.remove(this.taskStack_, functionStack_);
        }
    }
    // if no more functions: stop timers
    if (this.taskStack_.length == 1 &&
            this.taskStack_[0].functionStack_.length == 0) {
        this.disable_();
    }
};

/**
 * Start the internal timer functions
 * @this {lime.scheduleManager}
 * @private
 */
lime.scheduleManager.activate_ = function() {
    if (this.active_) return;
    // Todo: Firefox 4beta has a neat API window.mozRequestAnimationFrame()
    // This could possibly do better job in this case (but will ruin mockClock)
    // http://weblogs.mozillazine.org/roc/archives/2010/08/mozrequestanima.html

    this.lastRunTime_ = goog.now();

    if(lime.scheduleManager.USE_ANIMATION_FRAME){
        // mozilla
        if(window.mozRequestAnimationFrame){
            window.mozRequestAnimationFrame();
            this.beforePaintHandlerBinded_ = goog.bind(this.beforePaintHandler_,this);
            window.addEventListener('MozBeforePaint',this.beforePaintHandlerBinded_, false);
        }
        else { // webkit
            this.animationFrameHandlerBinded_ = goog.bind(this.animationFrameHandler_,this);
            window.webkitRequestAnimationFrame(this.animationFrameHandlerBinded_);
        }
    }
    else {
        this.intervalID_ = setInterval(goog.bind(this.stepTimer_, this),
            this.getDisplayRate());
    }
    this.active_ = true;
};

/**
 * Stop interval timer functions
 * @this {lime.scheduleManager}
 * @private
 */
lime.scheduleManager.disable_ = function() {
    if (!this.active_) return;

    if(lime.scheduleManager.USE_ANIMATION_FRAME){
        // mozilla
        if(window.mozRequestAnimationFrame){
            window.removeEventListener('MozBeforePaint',this.beforePaintHandlerBinded_, false);
        }
        else { //webkit
            window.webkitCancelRequestAnimationFrame(this.animationFrameHandlerBinded_);
        }
    }
    else {
        clearInterval(this.intervalID_);
    }
    this.active_ = false;
};

/**
 * Webkit implemtation of requestAnimationFrame handler.
 * @private
 */
lime.scheduleManager.animationFrameHandler_ = function(time){
    var delta = time - this.lastRunTime_;
    this.dispatch_(delta);
    this.lastRunTime_ = time;
    window.webkitRequestAnimationFrame(this.animationFrameHandlerBinded_);
}

/**
 * Mozilla implemtation of requestAnimationFrame handler.
 * @private
 */
lime.scheduleManager.beforePaintHandler_ = function(event){
    var delta = event.timeStamp - this.lastRunTime_;
    this.dispatch_(delta);
    this.lastRunTime_ = event.timeStamp;
    window.mozRequestAnimationFrame();
}

/**
 * Timer events step function that delegates to other objects waiting
 * @this {lime.scheduleManager}
 * @private
 */
lime.scheduleManager.stepTimer_ = function() {
    var t;
    var curTime = goog.now();
    var delta = curTime - this.lastRunTime_;
    if (delta < 0) delta = 1;
    this.dispatch_(delta);
    this.lastRunTime_ = curTime;
};

/**
 * Call all scheduled tasks
 * @this {lime.scheduleManager}
 * @param {number} delta Milliseconds since last run.
 * @private
 */
lime.scheduleManager.dispatch_ = function(delta){
    var i = this.taskStack_.length;
    while (--i >= 0) {
        this.taskStack_[i].step_(delta);
    }
}

/**
 * Change director's activity. Used for pausing updates when director is paused
 * @this {lime.scheduleManager}
 * @param {lime.Director} director Director.
 * @param {boolean} value Active or inactive?
 */
lime.scheduleManager.changeDirectorActivity = function(director, value) {
    var t, j = this.taskStack_.length;
    while (--j >= 0) {

    var t = this.taskStack_[j], f, context, d, i = t.functionStack_.length;
    while (--i >= 0) {
        f = t.functionStack_[i];
        context = f[2];
        if (goog.isFunction(context.getDirector)) {
            d = context.getDirector();
            if (d == director) {
                f[0] = value;
            }
        }
    }
    }
};

/**
 * Set up function to be called once after a delay
 * @param {function(number)} f Function to be called.
 * @param {object} context Context used when calling object.
 * @param {number} delay Delay before calling.
 */
lime.scheduleManager.callAfter = function(f, context, delay) {
    lime.scheduleManager.scheduleWithDelay(f, context, delay, 1);
};

/**
 * Set up function to be called repeatedly after a delay
 * @param {function(number)} f Function to be called.
 * @param {object} context Context used when calling object.
 * @param {number} delay Delay before calling.
 * @param {number} opt_limit Number of times to call.
 * @this {lime.scheduleManager}
 */
lime.scheduleManager.scheduleWithDelay = function(f, context,
        delay, opt_limit) {
    var task = new lime.scheduleManager.Task(delay, opt_limit);
    this.schedule(f, context, task);
};
<|endoftext|>


<commit_before>'use strict';

import * as Package from './../package.json';
import * as path from 'path';

import Plugin from 'maniajs-plugin';

/**
 * LocalRecords Plugin.
 */
export default class extends Plugin {

  constructor() {
    super();

    // Set the package stuff into the plugin context.
    this.name = Package.name;
    this.version = Package.version;
    this.directory = __dirname;

    // Add dependencies, enter module full id's (mostly npm package names) here.
    this.dependencies = [];

    // Plugin properties
    this.records = null;
    this.runs = [];

    this.recordlimit = 100;
    this.displaylimit = 50;
    this.chatdisplay = true;
    this.chatannounce = true;
  }

  /**
   * Init will be run once the plugin can register everything at the core.
   * From this point the {this.app} and all other injected variables are available.
   *
   * @return {Promise} The init should ALWAYS return a promise, the core will wait until the promise has been resolved!
   */
  init() {
    return new Promise((resolve, reject) => {
      if(!this.config) {
        if(this.config.hasOwnProperty('recordlimit') && this.config.recordlimit != '') {
          this.recordlimit = this.config.recordlimit;
        }

        if(this.config.hasOwnProperty('displaylimit') && this.config.displaylimit != '') {
          this.displaylimit = this.config.displaylimit;
        }

        if(this.config.hasOwnProperty('chatdisplay') && this.config.chatdisplay != '') {
          this.chatdisplay = this.config.chatdisplay;
        }

        if(this.config.hasOwnProperty('chatannounce') && this.config.chatannounce != '') {
          this.chatannounce = this.config.chatannounce;
        }
      }

      // Event
      this.server.on('map.begin',
        (params) => this.beginMap(params));

      this.server.on('trackmania.player.finish',
        (params) => this.playerFinish(params));

      this.server.on('trackmania.player.checkpoint',
        (params) => this.playerCheckpoint(params));

      this.server.command.on('skip', 1, (player, params) => {
        this.server.send().chat("$fffSkipping map...").exec();
        this.server.send().skip().exec();
      });

      this.loadRecords(this.maps.current).then(() => {
        resolve();
      }).catch((err) => {
        reject(err);
      });
    });
  }

  /**
   * Function called on map start: empties run overview and loads/displays Local Records.
   * @param map
   */
  beginMap(map) {
    this.runs = [];

    this.loadRecords(map);
  }

  /**
   * Function will display the local records based on the current map (does not use map input).
   *
   * @param map
   *
   * @returns {Promise}
   */
  loadRecords(map) {
    this.app.log.debug('New map: ' + this.maps.current.name + ' by ' + this.maps.current.author);
    this.server.send().chat('New map: ' + this.maps.current.name + '$z$s by ' + this.maps.current.author).exec();

    let Player = this.app.models.Player;

    return this.models.LocalRecord.findAll({
      where: {
        MapId: this.maps.current.id
      },
      include: [Player]
    }).then((records) => {
      this.records = records.sort((a, b) => a.score - b.score);

      if(this.chatdisplay) {
        var localRecords = '$39fLocal Records on $<$fff' + this.maps.current.name + '$>$39f';

        if(this.records.length > 0) {
          localRecords += ' (' + (this.records.length - 1) + '): ';
          for (var recordPos = 0; (recordPos < 10 && recordPos < this.records.length && recordPos < this.recordlimit); recordPos++) {
            localRecords += '$fff' + (recordPos + 1) + '$39f. $<$fff' + this.records[recordPos].Player.nickname + '$>$39f [$fff' + this.app.util.times.stringTime(this.records[recordPos].score) + '$39f] ';
          }
        } else {
          localRecords += ': $fffno records yet.';
        }

        this.server.send().chat(localRecords).exec();

        Object.keys(this.players.list).forEach((login) => {
          let player = this.players.list[login];
          var record = this.records.filter(function (rec) {
            return rec.PlayerId == player.id;
          });
          var text = '$090You currently do not have a Local Record on this map.';

          if (record.length == 1) {
            record = record[0];
            var recordIndex = (this.records.indexOf(record) + 1);
            if (recordIndex <= this.recordlimit) {
              text = '$090Your current Local Record is: $fff' + recordIndex + '$090. with a time of $fff' + this.app.util.times.stringTime(record.score) + '$090.';
            }
          }

          this.server.send().chat(text, {destination: login}).exec();
        });
      }
    });
  }

  /**
   * Function registers when a player finished and updates/inserts a local record if it's his/her best new time.
   *
   * @param finish
   */
  playerFinish(finish) {
    let login = finish.login;
    let time = finish.timeOrScore;

    if(time > 0) {
      let player = this.players.list[login];
      var record = this.records.filter(function (rec) { return rec.PlayerId == player.id; });

      if (record.length == 1) {
        // Already has a local record
        if(time < record[0].score) {
          var previousTime = record[0].score;
          var previousIndex = (this.records.indexOf(record[0]) + 1);

          record[0].score = time;
          if(this.runs[login]) {
            record[0].checkpoints = this.runs[login];
          } else {
            record[0].checkpoints = '';
          }
          record[0].save();

          this.records = this.records.sort((a, b) => a.score - b.score);

          if(this.chatannounce) {
            var newIndex = (this.records.indexOf(record[0]) + 1);

            var improvedRecordText = '';
            if(newIndex < previousIndex) {
              improvedRecordText = '$090$<$fff' + player.nickname + '$>$090 gained the $fff' + (newIndex) + '$090. Local Record, with a time of $fff' + this.app.util.times.stringTime(record[0].score) +
                  '$090 ($fff' + (previousIndex) + '$090. $fff' + this.app.util.times.stringTime(previousTime) + '$090/$fff-' + this.app.util.times.stringTime((previousTime - record[0].score)) + '$090)!';
            } else {
              improvedRecordText = '$090$<$fff' + player.nickname + '$>$090 improved his/her $fff' + (newIndex) + '$090., with a time of $fff' + this.app.util.times.stringTime(record[0].score) +
                  '$090 ($fff' + this.app.util.times.stringTime(previousTime) + '$090/$fff-' + this.app.util.times.stringTime((previousTime - record[0].score)) + '$090).';
            }

            if(newIndex <= this.displaylimit)
              this.server.send().chat(improvedRecordText).exec();
            else if(newIndex <= this.recordlimit)
              this.server.send().chat(improvedRecordText, {destination: login}).exec();
          }
        } else if(time == record[0].score) {
          if(this.chatannounce) {
            var equalledIndex = (this.records.indexOf(record[0]) + 1);
            var equalledRecordText = '$090$<$fff' + player.nickname + '$>$090 equalled his/her $fff' + equalledIndex + '$090. Local Record, with a time of $fff' + this.app.util.times.stringTime(record[0].score) + '$090...';

            if(equalledIndex <= this.displaylimit)
              this.server.send().chat(equalledRecordText).exec();
            else if(equalledIndex <= this.recordlimit)
              this.server.send().chat(equalledRecordText, {destination: login}).exec();
          }
        }
      } else {
        // Does not have a local record yet
        var newRecord = this.models.LocalRecord.build({
          score: time,
          Map: this.maps.current,
          Player: player,
          MapId: this.maps.current.id,
          PlayerId: player.id
        });

        newRecord.checkpoints = this.runs[login]? this.runs[login] : '';

        this.records.push(newRecord);
        this.records = this.records.sort((a, b) => a.score - b.score);

        if(this.chatannounce) {
          var newRecordIndex = (this.records.indexOf(newRecord) + 1);
          var newRecordText = '$090$<$fff' + player.nickname + '$>$090 drove the $fff' + newRecordIndex + '$090. Local Record, with a time of $fff' + this.app.util.times.stringTime(newRecord.score) + '$090!';

          if(newRecordIndex <= this.displaylimit)
            this.server.send().chat(newRecordText).exec();
          else
            this.server.send().chat(newRecordText, {destination: login}).exec();
        }

        newRecord.save();
      }
    }
  }

  /**
   * Function registers when a player passes a checkpoint and saves this in the current runs.
   * playerId: 0,
   * login: 1,
   * timeOrScore: 2,
   * curLap: 3,
   * checkpoint: 4
   *
   * @param checkpoint
   */
  playerCheckpoint(checkpoint) {
    let login = checkpoint.login;

    if(checkpoint.checkpoint == 0) {
      this.runs[login] = checkpoint.timeOrScore;
    } else {
      if(this.runs[login]) {
        this.runs[login] += ',' + checkpoint.timeOrScore;
      }
    }
  }
}
<commit_msg>Small change for improved record text.<commit_after>'use strict';

import * as Package from './../package.json';
import * as path from 'path';

import Plugin from 'maniajs-plugin';

/**
 * LocalRecords Plugin.
 */
export default class extends Plugin {

  constructor() {
    super();

    // Set the package stuff into the plugin context.
    this.name = Package.name;
    this.version = Package.version;
    this.directory = __dirname;

    // Add dependencies, enter module full id's (mostly npm package names) here.
    this.dependencies = [];

    // Plugin properties
    this.records = null;
    this.runs = [];

    this.recordlimit = 100;
    this.displaylimit = 50;
    this.chatdisplay = true;
    this.chatannounce = true;
  }

  /**
   * Init will be run once the plugin can register everything at the core.
   * From this point the {this.app} and all other injected variables are available.
   *
   * @return {Promise} The init should ALWAYS return a promise, the core will wait until the promise has been resolved!
   */
  init() {
    return new Promise((resolve, reject) => {
      if(!this.config) {
        if(this.config.hasOwnProperty('recordlimit') && this.config.recordlimit != '') {
          this.recordlimit = this.config.recordlimit;
        }

        if(this.config.hasOwnProperty('displaylimit') && this.config.displaylimit != '') {
          this.displaylimit = this.config.displaylimit;
        }

        if(this.config.hasOwnProperty('chatdisplay') && this.config.chatdisplay != '') {
          this.chatdisplay = this.config.chatdisplay;
        }

        if(this.config.hasOwnProperty('chatannounce') && this.config.chatannounce != '') {
          this.chatannounce = this.config.chatannounce;
        }
      }

      // Event
      this.server.on('map.begin',
        (params) => this.beginMap(params));

      this.server.on('trackmania.player.finish',
        (params) => this.playerFinish(params));

      this.server.on('trackmania.player.checkpoint',
        (params) => this.playerCheckpoint(params));

      this.server.command.on('skip', 1, (player, params) => {
        this.server.send().chat("$fffSkipping map...").exec();
        this.server.send().skip().exec();
      });

      this.loadRecords(this.maps.current).then(() => {
        resolve();
      }).catch((err) => {
        reject(err);
      });
    });
  }

  /**
   * Function called on map start: empties run overview and loads/displays Local Records.
   * @param map
   */
  beginMap(map) {
    this.runs = [];

    this.loadRecords(map);
  }

  /**
   * Function will display the local records based on the current map (does not use map input).
   *
   * @param map
   *
   * @returns {Promise}
   */
  loadRecords(map) {
    this.app.log.debug('New map: ' + this.maps.current.name + ' by ' + this.maps.current.author);
    this.server.send().chat('New map: ' + this.maps.current.name + '$z$s by ' + this.maps.current.author).exec();

    let Player = this.app.models.Player;

    return this.models.LocalRecord.findAll({
      where: {
        MapId: this.maps.current.id
      },
      include: [Player]
    }).then((records) => {
      this.records = records.sort((a, b) => a.score - b.score);

      if(this.chatdisplay) {
        var localRecords = '$39fLocal Records on $<$fff' + this.maps.current.name + '$>$39f';

        if(this.records.length > 0) {
          localRecords += ' (' + (this.records.length - 1) + '): ';
          for (var recordPos = 0; (recordPos < 10 && recordPos < this.records.length && recordPos < this.recordlimit); recordPos++) {
            localRecords += '$fff' + (recordPos + 1) + '$39f. $<$fff' + this.records[recordPos].Player.nickname + '$>$39f [$fff' + this.app.util.times.stringTime(this.records[recordPos].score) + '$39f] ';
          }
        } else {
          localRecords += ': $fffno records yet.';
        }

        this.server.send().chat(localRecords).exec();

        Object.keys(this.players.list).forEach((login) => {
          let player = this.players.list[login];
          var record = this.records.filter(function (rec) {
            return rec.PlayerId == player.id;
          });
          var text = '$090You currently do not have a Local Record on this map.';

          if (record.length == 1) {
            record = record[0];
            var recordIndex = (this.records.indexOf(record) + 1);
            if (recordIndex <= this.recordlimit) {
              text = '$090Your current Local Record is: $fff' + recordIndex + '$090. with a time of $fff' + this.app.util.times.stringTime(record.score) + '$090.';
            }
          }

          this.server.send().chat(text, {destination: login}).exec();
        });
      }
    });
  }

  /**
   * Function registers when a player finished and updates/inserts a local record if it's his/her best new time.
   *
   * @param finish
   */
  playerFinish(finish) {
    let login = finish.login;
    let time = finish.timeOrScore;

    if(time > 0) {
      let player = this.players.list[login];
      var record = this.records.filter(function (rec) { return rec.PlayerId == player.id; });

      if (record.length == 1) {
        // Already has a local record
        if(time < record[0].score) {
          var previousTime = record[0].score;
          var previousIndex = (this.records.indexOf(record[0]) + 1);

          record[0].score = time;
          if(this.runs[login]) {
            record[0].checkpoints = this.runs[login];
          } else {
            record[0].checkpoints = '';
          }
          record[0].save();

          this.records = this.records.sort((a, b) => a.score - b.score);

          if(this.chatannounce) {
            var newIndex = (this.records.indexOf(record[0]) + 1);

            var improvedRecordText = '';
            if(newIndex < previousIndex) {
              improvedRecordText = '$090$<$fff' + player.nickname + '$>$090 gained the $fff' + (newIndex) + '$090. Local Record, with a time of $fff' + this.app.util.times.stringTime(record[0].score) +
                  '$090 ($fff' + (previousIndex) + '$090. $fff' + this.app.util.times.stringTime(previousTime) + '$090/$fff-' + this.app.util.times.stringTime((previousTime - record[0].score)) + '$090)!';
            } else {
              improvedRecordText = '$090$<$fff' + player.nickname + '$>$090 improved his/her $fff' + (newIndex) + '$090. Local Record, with a time of $fff' + this.app.util.times.stringTime(record[0].score) +
                  '$090 ($fff' + this.app.util.times.stringTime(previousTime) + '$090/$fff-' + this.app.util.times.stringTime((previousTime - record[0].score)) + '$090).';
            }

            if(newIndex <= this.displaylimit)
              this.server.send().chat(improvedRecordText).exec();
            else if(newIndex <= this.recordlimit)
              this.server.send().chat(improvedRecordText, {destination: login}).exec();
          }
        } else if(time == record[0].score) {
          if(this.chatannounce) {
            var equalledIndex = (this.records.indexOf(record[0]) + 1);
            var equalledRecordText = '$090$<$fff' + player.nickname + '$>$090 equalled his/her $fff' + equalledIndex + '$090. Local Record, with a time of $fff' + this.app.util.times.stringTime(record[0].score) + '$090...';

            if(equalledIndex <= this.displaylimit)
              this.server.send().chat(equalledRecordText).exec();
            else if(equalledIndex <= this.recordlimit)
              this.server.send().chat(equalledRecordText, {destination: login}).exec();
          }
        }
      } else {
        // Does not have a local record yet
        var newRecord = this.models.LocalRecord.build({
          score: time,
          Map: this.maps.current,
          Player: player,
          MapId: this.maps.current.id,
          PlayerId: player.id
        });

        newRecord.checkpoints = this.runs[login]? this.runs[login] : '';

        this.records.push(newRecord);
        this.records = this.records.sort((a, b) => a.score - b.score);

        if(this.chatannounce) {
          var newRecordIndex = (this.records.indexOf(newRecord) + 1);
          var newRecordText = '$090$<$fff' + player.nickname + '$>$090 drove the $fff' + newRecordIndex + '$090. Local Record, with a time of $fff' + this.app.util.times.stringTime(newRecord.score) + '$090!';

          if(newRecordIndex <= this.displaylimit)
            this.server.send().chat(newRecordText).exec();
          else
            this.server.send().chat(newRecordText, {destination: login}).exec();
        }

        newRecord.save();
      }
    }
  }

  /**
   * Function registers when a player passes a checkpoint and saves this in the current runs.
   * playerId: 0,
   * login: 1,
   * timeOrScore: 2,
   * curLap: 3,
   * checkpoint: 4
   *
   * @param checkpoint
   */
  playerCheckpoint(checkpoint) {
    let login = checkpoint.login;

    if(checkpoint.checkpoint == 0) {
      this.runs[login] = checkpoint.timeOrScore;
    } else {
      if(this.runs[login]) {
        this.runs[login] += ',' + checkpoint.timeOrScore;
      }
    }
  }
}
<|endoftext|>


<commit_before>/**
 *  FsTools
 *
 *  Collection of FS related tools, that stdlib lack of.
 **/

'use strict';

// stdlib
var fs = require('fs');
var path_join = require('path').join;
var path_exists = require('path').exists;
var path_normalize = require('path').normalize;
var dirname = require('path').dirname;

// 3rd-party
var async = require('async');
var _ = require('underscore');

// epxorts: walk, mkdir, copy, remove
var fstools = module.exports = {};

// INTERNAL HELPERS
////////////////////////////////////////////////////////////////////////////////

//  walk_flat(path, iterator, callback) -> void
//  - path (String): Path to iterate through
//  - iterator (Function): Will be fired on each element within `path`
//  - callback (Function): Will be fired once all files were processed
//
//  Walks through given `path` and calls `iterator(path, stats, callback)` for
//  each found entry (regardless to it's type) and waits for all `callback`s
//  (passed to iterator) to be fired. After all callbacks were fired, fires
//  `callback` (given to walk_flat itself).
//
//  NOTICE: It walks through single dimension of file system - it won't go into
//          found sub-directories. See `walk_recursive` for this puprpose.
//
//  Example:
//
//    walk_flat('/home/nodeca', function (path, stats, callback) {
//      if ('/home/nodeca/secrets.yml' === path) {
//        callback(Error("There is secrets file."));
//        return;
//      }
//
//      if ('/home/nodeca/xxx' === path && stats.isDirectory()) {
//        callback(Error("Path contains xxx directory."));
//        return;
//      }
//
//      callback();
//    }, function (err) {
//      if (err) console.error(err);
//      console.log('Done');
//    });
function walk_flat(path, iterator, callback) {
  async.waterfall([
    async.apply(fs.readdir, path),
    function (files, next) {
      async.forEach(files, function (file, next_file) {
        var filename = path_join(path, file);

        async.waterfall([
          async.apply(fs.lstat, filename),
          function (stats, callback) {
            iterator(filename, stats, callback);
          }
        ], next_file);
      }, next);
    }
  ], callback);
}

//  walk_recursive(path, iterator, callback) -> void
//
//  Proxies call to `walk_flat`, but wraps `iterator` so that it will
//  recursively go deeper inside each found directoy instead of calling
//  iterator on it.
function walk_recursive(path, iterator, callback) {
  // walk through given path
  walk_flat(path, function (path, stats, callback) {
    // go deeper inside
    if (stats.isDirectory()) {
      walk_recursive(path, iterator, callback);
      return;
    }

    // call iterator if not directory
    iterator(path, stats, callback);
  }, callback);
}

var copy; // hack for lint :))

function copy_file(src, dst, callback) {
  var ifd, ofd;

  // create streams
  ifd = fs.createReadStream(src, {bufferSize: 64 * 1024}).on('error', callback);
  ofd = fs.createWriteStream(dst).on('error', callback).on('close', callback);

  // pipe src to dst
  ifd.pipe(ofd);
}

function copy_symlink(src, dst, callback) {
  // read where symlinks points to
  fs.readlink(src, function (err, linkpath) {
    if (err) {
      callback(err);
      return;
    }

    // create symlink
    fs.symlink(linkpath, dst, callback);
  });
}

function copy_directory(src, dst, callback) {
  fstools.mkdir(dst, '0755', function (err) {
    if (err) {
      callback(err);
      return;
    }

    walk_flat(src, function (sub_src, sub_stats, next) {
      copy(sub_src, dst + sub_src.replace(src, ''), sub_stats, next);
    }, callback);
  });
}

// copy src to dst recursively
copy = function copy(src, dst, stats, callback) {
  var chmod, done;

  // chmod dst
  chmod = async.apply(fs.chmod, dst, stats.mode.toString(8).slice(-4));

  // reject async.series' results
  done = function (err, results) { callback(err); };

  // *** file
  if (stats.isFile()) {
    async.series([async.apply(copy_file, src, dst), chmod], done);
    return;
  }

  // *** symbolic link
  if (stats.isSymbolicLink()) {
    async.series([async.apply(copy_symlink, src, dst), chmod], done);
    return;
  }

  // *** directory
  if (stats.isDirectory()) {
    async.series([async.apply(copy_directory, src, dst), chmod], done);
    return;
  }

  // *** unsupported src
  callback(new Error("Unsupported type of the source"));
};

// PUBLIC API
////////////////////////////////////////////////////////////////////////////////

/**
 *  FsTools.walk(path, pattern, iterator[, callback]) -> void
 *  FsTools.walk(path, iterator[, callback]) -> void
 *
 *  Walks throught all files withing `path` (including sub-dirs) and calls
 *  `iterator` on each found file (or block device etc.) matching `pattern`.
 *  If no `pattern` was given - will fire call `iterator` for every single
 *  path found. After all iterations will call `callback` (if it was specified)
 *  with passing `error` as first arguemtn if there was an error.
 *
 *  ##### Iterator
 *
 *  All iterations are running within promise. So `callback` given to the `walk`
 *  will fire only after all `iterator` callbacks willnotify they finished their
 *  work:
 *
 *      var iterator = function (path, stats, callback) {
 *        // ... do something
 *        if (err) {
 *          // ... if error occured we can "stop" walker
 *          callback(err);
 *          return;
 *        }
 *        // ... if everything is good and finished notify walker we're done
 *        callback();
 *      };
 *
 *  Iterator is called with following arguments:
 *
 *  - `path` (String): Full path of the found element (e.g. `/foo/bar.txt`)
 *  - `stats` (fs.Stats): Stats object of found path
 *  - `callback` (Function): Callback function to call after path processing
 *
 *
 *  ##### Example
 *
 *      fstools.walk('/home/nodeca', function (path, stats, callback) {
 *        if (stats.isBlockDevice()) {
 *          callback(Error("WTF? Block devices are not expetcted in my room"));
 *          return;
 *        }
 *
 *        if (stats.isSocket()) {
 *          console.log("Finally I found my socket");
 *        }
 *
 *        callback();
 *      }, function (err) {
 *        if (err) {
 *          // shit happens!
 *          console.error(err);
 *          process.exit(1);
 *        }
 *
 *        console.log("Hooray! We're done!");
 *      });
 *
 *
 *  ##### Example (using pattern matching)
 *
 *      fstools.walk('/home/nodeca', '\.yml$', function (path, stats, callback) {
 *        fs.readFile(path, 'utf-8', funtion (err, str) {
 *          if (err) {
 *            callback(err);
 *            return;
 *          }
 *
 *          console.log(str);
 *          callback();
 *        });
 *      }, function (err) {
 *        if (err) {
 *          console.error(err);
 *        }
 *
 *        console.log('Done!');
 *      });
 **/
fstools.walk = function (path, pattern, iterator, callback) {
  var match; // function that tells whenever iterator need to be called or not

  if (_.isFunction(pattern)) {
    // Scenario: FileWalker(path, iterator[, callback])
    callback = iterator;
    iterator = pattern;
    match = function () { return true; };
  } else {
    // Scenario: FileWalker(path, pattern, iterator[, callback])
    pattern = new RegExp(pattern);
    match = function (path) { return pattern.test(path); };
  }

  if (!_.isFunction(callback)) {
    // Scenario: FileWalker(path, pattern, iterator)
    //       or: FileWalker(path, iterator)
    callback = function () {};
  }

  path = path_normalize(path);
  path_exists(path, function (exists) {
    if (!exists) {
      callback();
      return;
    }

    // start walking
    walk_recursive(path_normalize(path), function (path, stats, next) {
      // call iterator
      if (match(path)) {
        iterator(path, stats, next);
        return;
      }

      // skip entry
      next();
    }, callback);
  });
};

/**
 *  FsTools.remove(path, callback) -> void
 *  - path (String): Path to remove
 *  - callback (Function): Fired after path was removed
 *
 *  Removes given `path`. If it was a directory will remove it recursively,
 *  similar to UNIX' `rm -rf <path>`. After all will fire `callback(err)` with
 *  an error if there were any.
 *
 *  If given `path` was file - will proxy call to `fs.unlink`.
 *
 *
 *  ##### Example
 *
 *      fstools.remove('/home/nodeca/trash', function (err) {
 *        if (err) {
 *          console.log("U can't touch that");
 *          console.err(err);
 *          process.exit(1);
 *        } else {
 *          console.log("It's Hammer time");
 *          process.exit(0);
 *        }
 *      });
 **/
fstools.remove = function (path, callback) {
  path = path_normalize(path);
  fs.lstat(path, function (err, stats) {
    if (err) {
      // file/dir not exists - no need to do anything
      if ('ENOENT' === err.code) {
        callback(null);
        return;
      }

      // unknown error - can't continue
      callback(err);
      return;
    }

    if (!stats.isDirectory()) {
      fs.unlink(path, callback);
      return;
    }

    async.series([
      async.apply(walk_flat, path, function (path, stats, next) {
        fstools.remove(path, next);
      }),
      async.apply(fs.rmdir, path)
    ], function (err, results) {
      callback(err);
    });
  });
};

/**
 *  FsTools.mkdir(path, mode, callback) -> void
 *  FsTools.mkdir(path, callback) -> void
 *  - path (String): Path to create
 *  - mode (String|Number): Permission mode of new directory. See stdlib
 *    fs.mkdir for details. Default: '0755'.
 *  - callback (Function): Fired after path was created
 *
 *  Creates given path, creating parents recursively if needed.
 *  Similar to UNIX' `mkdir -pf <path>`. After all will fire `callback(err)` with
 *  an error if there were any.
 *
 *
 *  ##### Example
 *
 *      fstools.mkdir('/home/nodeca/media/xxx', function (err) {
 *        if (err) {
 *          console.log("Can't' create directory");
 *          console.err(err);
 *          process.exit(1);
 *        } else {
 *          console.log("We can now store some romantic movies here");
 *          process.exit(0);
 *        }
 *      });
 **/
fstools.mkdir = function (path, mode, callback) {
  if (undefined === callback && _.isFunction(mode)) {
    callback = mode;
    mode = '0755';
  }

  path = path_normalize(path);
  path_exists(path, function (exists) {
    var parent;

    if (exists) {
      callback(null);
      return;
    }

    parent = dirname(path);
    fstools.mkdir(parent, function (err) {
      if (err) {
        callback(err);
        return;
      }

      fs.mkdir(path, mode, function (err) {
        // EEXIST is not error in our case
        // but a race condition :((
        if (err && 'EEXIST' === err.code) {
          callback(null);
          return;
        }

        // fallback to default behavior
        callback(err);
      });
    });
  });
};

/**
 *  FsTools.copy(src, dst, callback) -> void
 *  - src (String): Source file
 *  - dst (String): Destination file
 *  - callback (Function): Fired after path has been copied
 *
 *  Copies `src` to `dst`, creates directory for given `dst` with
 *  [[FsTools.mkdir]] if needed. Fires `callback(err)` upon
 *  completion.
 *
 *  ##### Example
 *
 *      var src = '/home/nodeca/secrets.yml',
 *          dst = '/home/nodeca/very/deep/secrets/main.yml';
 *
 *      fstools.copy(src, dst, function (err) {
 *        if (err) {
 *          console.log("Failed copy " + src + " into " + dst);
 *          console.err(err);
 *          process.exit(1);
 *        } else {
 *          console.log("Done!");
 *          process.exit(0);
 *        }
 *      });
 **/
fstools.copy = function (src, dst, callback) {
  src = path_normalize(src);
  dst = path_normalize(dst);

  // sad but true - people make mistakes...
  if (src === dst) {
    callback(null);
    return;
  }

  fs.lstat(src, function (err, stats) {
    if (err) {
      callback(err);
      return;
    }

    fstools.mkdir(dirname(dst), function (err) {
      if (err) {
        callback(err);
        return;
      }

      copy(src, dst, stats, callback);
    });
  });
};

////////////////////////////////////////////////////////////////////////////////
// vim:ts=2:sw=2
////////////////////////////////////////////////////////////////////////////////
<commit_msg>Move copy() worker under fstools.copy()<commit_after>/**
 *  FsTools
 *
 *  Collection of FS related tools, that stdlib lack of.
 **/

'use strict';

// stdlib
var fs = require('fs');
var path_join = require('path').join;
var path_exists = require('path').exists;
var path_normalize = require('path').normalize;
var dirname = require('path').dirname;

// 3rd-party
var async = require('async');
var _ = require('underscore');

// epxorts: walk, mkdir, copy, remove
var fstools = module.exports = {};

// INTERNAL HELPERS
////////////////////////////////////////////////////////////////////////////////

//  walk_flat(path, iterator, callback) -> void
//  - path (String): Path to iterate through
//  - iterator (Function): Will be fired on each element within `path`
//  - callback (Function): Will be fired once all files were processed
//
//  Walks through given `path` and calls `iterator(path, stats, callback)` for
//  each found entry (regardless to it's type) and waits for all `callback`s
//  (passed to iterator) to be fired. After all callbacks were fired, fires
//  `callback` (given to walk_flat itself).
//
//  NOTICE: It walks through single dimension of file system - it won't go into
//          found sub-directories. See `walk_recursive` for this puprpose.
//
//  Example:
//
//    walk_flat('/home/nodeca', function (path, stats, callback) {
//      if ('/home/nodeca/secrets.yml' === path) {
//        callback(Error("There is secrets file."));
//        return;
//      }
//
//      if ('/home/nodeca/xxx' === path && stats.isDirectory()) {
//        callback(Error("Path contains xxx directory."));
//        return;
//      }
//
//      callback();
//    }, function (err) {
//      if (err) console.error(err);
//      console.log('Done');
//    });
function walk_flat(path, iterator, callback) {
  async.waterfall([
    async.apply(fs.readdir, path),
    function (files, next) {
      async.forEach(files, function (file, next_file) {
        var filename = path_join(path, file);

        async.waterfall([
          async.apply(fs.lstat, filename),
          function (stats, callback) {
            iterator(filename, stats, callback);
          }
        ], next_file);
      }, next);
    }
  ], callback);
}

//  walk_recursive(path, iterator, callback) -> void
//
//  Proxies call to `walk_flat`, but wraps `iterator` so that it will
//  recursively go deeper inside each found directoy instead of calling
//  iterator on it.
function walk_recursive(path, iterator, callback) {
  // walk through given path
  walk_flat(path, function (path, stats, callback) {
    // go deeper inside
    if (stats.isDirectory()) {
      walk_recursive(path, iterator, callback);
      return;
    }

    // call iterator if not directory
    iterator(path, stats, callback);
  }, callback);
}

function copy_file(src, dst, callback) {
  var ifd, ofd;

  // create streams
  ifd = fs.createReadStream(src, {bufferSize: 64 * 1024}).on('error', callback);
  ofd = fs.createWriteStream(dst).on('error', callback).on('close', callback);

  // pipe src to dst
  ifd.pipe(ofd);
}

function copy_symlink(src, dst, callback) {
  // read where symlinks points to
  fs.readlink(src, function (err, linkpath) {
    if (err) {
      callback(err);
      return;
    }

    // create symlink
    fs.symlink(linkpath, dst, callback);
  });
}

function copy_directory(src, dst, callback) {
  fstools.mkdir(dst, '0755', function (err) {
    if (err) {
      callback(err);
      return;
    }

    walk_flat(src, function (sub_src, sub_stats, next) {
      fstools.copy(sub_src, dst + sub_src.replace(src, ''), next);
    }, callback);
  });
}

// PUBLIC API
////////////////////////////////////////////////////////////////////////////////

/**
 *  FsTools.walk(path, pattern, iterator[, callback]) -> void
 *  FsTools.walk(path, iterator[, callback]) -> void
 *
 *  Walks throught all files withing `path` (including sub-dirs) and calls
 *  `iterator` on each found file (or block device etc.) matching `pattern`.
 *  If no `pattern` was given - will fire call `iterator` for every single
 *  path found. After all iterations will call `callback` (if it was specified)
 *  with passing `error` as first arguemtn if there was an error.
 *
 *  ##### Iterator
 *
 *  All iterations are running within promise. So `callback` given to the `walk`
 *  will fire only after all `iterator` callbacks willnotify they finished their
 *  work:
 *
 *      var iterator = function (path, stats, callback) {
 *        // ... do something
 *        if (err) {
 *          // ... if error occured we can "stop" walker
 *          callback(err);
 *          return;
 *        }
 *        // ... if everything is good and finished notify walker we're done
 *        callback();
 *      };
 *
 *  Iterator is called with following arguments:
 *
 *  - `path` (String): Full path of the found element (e.g. `/foo/bar.txt`)
 *  - `stats` (fs.Stats): Stats object of found path
 *  - `callback` (Function): Callback function to call after path processing
 *
 *
 *  ##### Example
 *
 *      fstools.walk('/home/nodeca', function (path, stats, callback) {
 *        if (stats.isBlockDevice()) {
 *          callback(Error("WTF? Block devices are not expetcted in my room"));
 *          return;
 *        }
 *
 *        if (stats.isSocket()) {
 *          console.log("Finally I found my socket");
 *        }
 *
 *        callback();
 *      }, function (err) {
 *        if (err) {
 *          // shit happens!
 *          console.error(err);
 *          process.exit(1);
 *        }
 *
 *        console.log("Hooray! We're done!");
 *      });
 *
 *
 *  ##### Example (using pattern matching)
 *
 *      fstools.walk('/home/nodeca', '\.yml$', function (path, stats, callback) {
 *        fs.readFile(path, 'utf-8', funtion (err, str) {
 *          if (err) {
 *            callback(err);
 *            return;
 *          }
 *
 *          console.log(str);
 *          callback();
 *        });
 *      }, function (err) {
 *        if (err) {
 *          console.error(err);
 *        }
 *
 *        console.log('Done!');
 *      });
 **/
fstools.walk = function (path, pattern, iterator, callback) {
  var match; // function that tells whenever iterator need to be called or not

  if (_.isFunction(pattern)) {
    // Scenario: FileWalker(path, iterator[, callback])
    callback = iterator;
    iterator = pattern;
    match = function () { return true; };
  } else {
    // Scenario: FileWalker(path, pattern, iterator[, callback])
    pattern = new RegExp(pattern);
    match = function (path) { return pattern.test(path); };
  }

  if (!_.isFunction(callback)) {
    // Scenario: FileWalker(path, pattern, iterator)
    //       or: FileWalker(path, iterator)
    callback = function () {};
  }

  path = path_normalize(path);
  path_exists(path, function (exists) {
    if (!exists) {
      callback();
      return;
    }

    // start walking
    walk_recursive(path_normalize(path), function (path, stats, next) {
      // call iterator
      if (match(path)) {
        iterator(path, stats, next);
        return;
      }

      // skip entry
      next();
    }, callback);
  });
};

/**
 *  FsTools.remove(path, callback) -> void
 *  - path (String): Path to remove
 *  - callback (Function): Fired after path was removed
 *
 *  Removes given `path`. If it was a directory will remove it recursively,
 *  similar to UNIX' `rm -rf <path>`. After all will fire `callback(err)` with
 *  an error if there were any.
 *
 *  If given `path` was file - will proxy call to `fs.unlink`.
 *
 *
 *  ##### Example
 *
 *      fstools.remove('/home/nodeca/trash', function (err) {
 *        if (err) {
 *          console.log("U can't touch that");
 *          console.err(err);
 *          process.exit(1);
 *        } else {
 *          console.log("It's Hammer time");
 *          process.exit(0);
 *        }
 *      });
 **/
fstools.remove = function (path, callback) {
  path = path_normalize(path);
  fs.lstat(path, function (err, stats) {
    if (err) {
      // file/dir not exists - no need to do anything
      if ('ENOENT' === err.code) {
        callback(null);
        return;
      }

      // unknown error - can't continue
      callback(err);
      return;
    }

    if (!stats.isDirectory()) {
      fs.unlink(path, callback);
      return;
    }

    async.series([
      async.apply(walk_flat, path, function (path, stats, next) {
        fstools.remove(path, next);
      }),
      async.apply(fs.rmdir, path)
    ], function (err, results) {
      callback(err);
    });
  });
};

/**
 *  FsTools.mkdir(path, mode, callback) -> void
 *  FsTools.mkdir(path, callback) -> void
 *  - path (String): Path to create
 *  - mode (String|Number): Permission mode of new directory. See stdlib
 *    fs.mkdir for details. Default: '0755'.
 *  - callback (Function): Fired after path was created
 *
 *  Creates given path, creating parents recursively if needed.
 *  Similar to UNIX' `mkdir -pf <path>`. After all will fire `callback(err)` with
 *  an error if there were any.
 *
 *
 *  ##### Example
 *
 *      fstools.mkdir('/home/nodeca/media/xxx', function (err) {
 *        if (err) {
 *          console.log("Can't' create directory");
 *          console.err(err);
 *          process.exit(1);
 *        } else {
 *          console.log("We can now store some romantic movies here");
 *          process.exit(0);
 *        }
 *      });
 **/
fstools.mkdir = function (path, mode, callback) {
  if (undefined === callback && _.isFunction(mode)) {
    callback = mode;
    mode = '0755';
  }

  path = path_normalize(path);
  path_exists(path, function (exists) {
    var parent;

    if (exists) {
      callback(null);
      return;
    }

    parent = dirname(path);
    fstools.mkdir(parent, function (err) {
      if (err) {
        callback(err);
        return;
      }

      fs.mkdir(path, mode, function (err) {
        // EEXIST is not error in our case
        // but a race condition :((
        if (err && 'EEXIST' === err.code) {
          callback(null);
          return;
        }

        // fallback to default behavior
        callback(err);
      });
    });
  });
};

/**
 *  FsTools.copy(src, dst, callback) -> void
 *  - src (String): Source file
 *  - dst (String): Destination file
 *  - callback (Function): Fired after path has been copied
 *
 *  Copies `src` to `dst`, creates directory for given `dst` with
 *  [[FsTools.mkdir]] if needed. Fires `callback(err)` upon
 *  completion.
 *
 *  ##### Example
 *
 *      var src = '/home/nodeca/secrets.yml',
 *          dst = '/home/nodeca/very/deep/secrets/main.yml';
 *
 *      fstools.copy(src, dst, function (err) {
 *        if (err) {
 *          console.log("Failed copy " + src + " into " + dst);
 *          console.err(err);
 *          process.exit(1);
 *        } else {
 *          console.log("Done!");
 *          process.exit(0);
 *        }
 *      });
 **/
fstools.copy = function (src, dst, callback) {
  src = path_normalize(src);
  dst = path_normalize(dst);

  // sad but true - people make mistakes...
  if (src === dst) {
    callback(null);
    return;
  }

  fs.lstat(src, function (err, stats) {
    if (err) {
      callback(err);
      return;
    }

    fstools.mkdir(dirname(dst), function (err) {
      var chmod, done;

      if (err) {
        callback(err);
        return;
      }

      // chmod dst
      chmod = async.apply(fs.chmod, dst, stats.mode.toString(8).slice(-4));

      // reject async.series' results
      done = function (err, results) { callback(err); };

      // *** file
      if (stats.isFile()) {
        async.series([async.apply(copy_file, src, dst), chmod], done);
        return;
      }

      // *** symbolic link
      if (stats.isSymbolicLink()) {
        async.series([async.apply(copy_symlink, src, dst), chmod], done);
        return;
      }

      // *** directory
      if (stats.isDirectory()) {
        async.series([async.apply(copy_directory, src, dst), chmod], done);
        return;
      }

      // *** unsupported src
      callback(new Error("Unsupported type of the source"));
    });
  });
};

////////////////////////////////////////////////////////////////////////////////
// vim:ts=2:sw=2
////////////////////////////////////////////////////////////////////////////////
<|endoftext|>


<commit_before>/**
# tabs

A simple tab component. It shows one of its children at a time and
makes one tab based on the `name` attribute of each child.

The value of the tab component is the index of the currently visible
body.

## TODO

Implement <tab-body> and <tab-button> to allow fine-grained styling
and rich content in tabs, and assignment of specific values to tabs
(versus indices).

```
<b8r-tab-selector closeable style="height: 100%">
  <div name="first" style="padding: 20px">first tab content</div>
  <div name="second tab has a long name" style="padding: 20px">second tab content</div>
  <div name="third" style="padding: 20px">third tab content</div>
</b8r-tab-selector>
<script>
  await import('../web-components/tabs.js');
</script>
```
*/
/* global requestAnimationFrame */

import {
  fragment,
  div,
  span,
  slot,
  button,
  makeWebComponent
} from '../lib/web-components.js'

const TabSelector = makeWebComponent('b8r-tab-selector', {
  attributes: {
    value: 0,
    closeable: false
  },
  style: {
    ':host': {
      display: 'flex',
      flexDirection: 'column'
    },
    slot: {
      position: 'relative',
      display: 'block',
      background: 'white',
      border: '1px solid #ccc',
      borderRadius: '2px',
      flexGrow: 1
    },
    '.tabs': {
      borderColor: '#ccc',
      padding: '5px 5px 0 5px',
      display: 'flex',
      flexShrink: 0,
      position: 'relative'
    },
    '.tabs > span': {
      flex: '1 1 50%',
      display: 'flex',
      maxWidth: '50%',
      background: '#ddd',
      padding: '5px 10px',
      borderRadius: '5px 5px 0 0',
      border: '1px solid #ccc',
      borderBottom: '1px solid transparent',
      cursor: 'default',
      margin: '-1px',
      whiteSpace: 'nowrap'
    },
    '.tabs > span > span': {
      flexGrow: 1,
      whitespace: 'nowrap',
      overflow: 'hidden',
      textOverflow: 'ellipsis'
    },
    '.tabs > span > span+button': {
      border: 0,
      background: 'transparent',
      flexGrow: 0,
      flexShrink: 0,
      margin: '0 -5px'
    },
    '.tabs > .selected': {
      background: 'white',
      zIndex: '2',
      border: '1px solid #ccc',
      borderBottom: '1px solid white',
      transform: 'translateY(1px)'
    }
  },
  eventHandlers: {
    childListChange () {
      this.buildTabs()
    }
  },
  methods: {
    onMount () {
      this.buildTabs()
    },
    pickTab (idx) {
      this.value = idx
      const tab = this.shadowRoot.querySelector('.tabs').children[idx]
      requestAnimationFrame(() => tab.focus())
    },
    buildTabs () {
      const tabs = this.shadowRoot.querySelector('.tabs')
      // note that this is explicitly supporting b8r list bindings,
      // but should cause no problems for vanilla js.
      const bodies = [...this.children].filter(body => !body.dataset.list)
      tabs.innerHTML = ''
      const attributes = { tabIndex: 0 }
      bodies.forEach((body, idx) => {
        const name = body.getAttribute('name') || 'untitled'
        const closeButton = button({ content: '×' })
        const content = [span({ content: name }), closeButton]
        const tab = span({ attributes, content })
        body._tab = tab
        tab.addEventListener('keydown', (evt) => {
          switch (evt.code) {
            case 'Space':
              if (evt.composedPath()[0].matches('button')) {
                this.value -= 1
                body.remove()
              } else {
                this.pickTab(idx)
              }
              break
            case 'ArrowRight':
              this.pickTab(idx < bodies.length - 1 ? idx + 1 : 0)
              break
            case 'ArrowLeft':
              this.pickTab(idx ? idx - 1 : bodies.length - 1)
              break
          }
        })
        tab.addEventListener('click', (evt) => {
          if (evt.composedPath()[0].matches('button')) {
            this.value -= 1
            body.remove()
          } else {
            this.pickTab(idx)
          }
        })
        tabs.appendChild(tab)
      })
      this._bodies = bodies
    },
    render () {
      const value = this.value >= 0 && this.value <= this._bodies.length
        ? this.value
        : 0
      const closeButtonDisplay = this.closeable ? '' : 'none'
      this._bodies.forEach((body, idx) => {
        const selected = parseInt(idx, 10) === parseInt(value, 10)
        body.style.display = selected ? '' : 'none'
        body._tab.classList.toggle('selected', selected)
        body._tab.querySelector('button').style.display = closeButtonDisplay
      })
    }
  },
  content: fragment(
    div({ classes: ['tabs'] }),
    slot()
  ),
  ariaRole: 'rich text'
})

export {
  TabSelector
}
<commit_msg>added title attribute to close buttons<commit_after>/**
# tabs

A simple tab component. It shows one of its children at a time and
makes one tab based on the `name` attribute of each child.

The value of the tab component is the index of the currently visible
body.

## TODO

Implement <tab-body> and <tab-button> to allow fine-grained styling
and rich content in tabs, and assignment of specific values to tabs
(versus indices).

```
<b8r-tab-selector closeable style="height: 100%">
  <div name="first" style="padding: 20px">first tab content</div>
  <div name="second tab has a long name" style="padding: 20px">second tab content</div>
  <div name="third" style="padding: 20px">third tab content</div>
</b8r-tab-selector>
<script>
  await import('../web-components/tabs.js');
</script>
```
*/
/* global requestAnimationFrame */

import {
  fragment,
  div,
  span,
  slot,
  button,
  makeWebComponent
} from '../lib/web-components.js'

const TabSelector = makeWebComponent('b8r-tab-selector', {
  attributes: {
    value: 0,
    closeable: false
  },
  style: {
    ':host': {
      display: 'flex',
      flexDirection: 'column'
    },
    slot: {
      position: 'relative',
      display: 'block',
      background: 'white',
      border: '1px solid #ccc',
      borderRadius: '2px',
      flexGrow: 1
    },
    '.tabs': {
      borderColor: '#ccc',
      padding: '5px 5px 0 5px',
      display: 'flex',
      flexShrink: 0,
      position: 'relative'
    },
    '.tabs > span': {
      flex: '1 1 50%',
      display: 'flex',
      maxWidth: '50%',
      background: '#ddd',
      padding: '5px 10px',
      borderRadius: '5px 5px 0 0',
      border: '1px solid #ccc',
      borderBottom: '1px solid transparent',
      cursor: 'default',
      margin: '-1px',
      whiteSpace: 'nowrap'
    },
    '.tabs > span > span': {
      flexGrow: 1,
      whitespace: 'nowrap',
      overflow: 'hidden',
      textOverflow: 'ellipsis'
    },
    '.tabs > span > span+button': {
      border: 0,
      background: 'transparent',
      flexGrow: 0,
      flexShrink: 0,
      margin: '0 -5px'
    },
    '.tabs > .selected': {
      background: 'white',
      zIndex: '2',
      border: '1px solid #ccc',
      borderBottom: '1px solid white',
      transform: 'translateY(1px)'
    }
  },
  eventHandlers: {
    childListChange () {
      this.buildTabs()
    }
  },
  methods: {
    onMount () {
      this.buildTabs()
    },
    pickTab (idx) {
      this.value = idx
      const tab = this.shadowRoot.querySelector('.tabs').children[idx]
      requestAnimationFrame(() => tab.focus())
    },
    buildTabs () {
      const tabs = this.shadowRoot.querySelector('.tabs')
      // note that this is explicitly supporting b8r list bindings,
      // but should cause no problems for vanilla js.
      const bodies = [...this.children].filter(body => !body.dataset.list)
      tabs.innerHTML = ''
      const attributes = { tabIndex: 0 }
      const closeButtonAttributes = { title: 'close tab' }
      bodies.forEach((body, idx) => {
        const name = body.getAttribute('name') || 'untitled'
        const closeButton = button({ attributes: closeButtonAttributes, content: '×' })
        const content = [span({ content: name }), closeButton]
        const tab = span({ attributes, content })
        body._tab = tab
        tab.addEventListener('keydown', (evt) => {
          switch (evt.code) {
            case 'Space':
              if (evt.composedPath()[0].matches('button')) {
                this.value -= 1
                body.remove()
              } else {
                this.pickTab(idx)
              }
              break
            case 'ArrowRight':
              this.pickTab(idx < bodies.length - 1 ? idx + 1 : 0)
              break
            case 'ArrowLeft':
              this.pickTab(idx ? idx - 1 : bodies.length - 1)
              break
          }
        })
        tab.addEventListener('click', (evt) => {
          if (evt.composedPath()[0].matches('button')) {
            this.value -= 1
            body.remove()
          } else {
            this.pickTab(idx)
          }
        })
        tabs.appendChild(tab)
      })
      this._bodies = bodies
    },
    render () {
      const value = this.value >= 0 && this.value <= this._bodies.length
        ? this.value
        : 0
      const closeButtonDisplay = this.closeable ? '' : 'none'
      this._bodies.forEach((body, idx) => {
        const selected = parseInt(idx, 10) === parseInt(value, 10)
        body.style.display = selected ? '' : 'none'
        body._tab.classList.toggle('selected', selected)
        body._tab.querySelector('button').style.display = closeButtonDisplay
      })
    }
  },
  content: fragment(
    div({ classes: ['tabs'] }),
    slot()
  ),
  ariaRole: 'rich text'
})

export {
  TabSelector
}
<|endoftext|>