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

goog.provide('shakaDemo.BoolInput');
goog.provide('shakaDemo.DatalistInput');
goog.provide('shakaDemo.Input');
goog.provide('shakaDemo.NumberInput');
goog.provide('shakaDemo.SelectInput');
goog.provide('shakaDemo.TextInput');

/**
 * Creates and contains the MDL elements of a type of input.
 */
shakaDemo.Input = class {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} inputType The element type for the input object.
   * @param {string} containerType The element type for the container containing
   *   the input object.
   * @param {string} extraType The element type for the "sibling element" to the
   *   input object. If null, it adds no such element.
   * @param {function(!Element)} onChange
   */
  constructor(parentContainer, inputType, containerType, extraType, onChange) {
    /** @private {!Element} */
    this.container_ = document.createElement(containerType);
    parentContainer.latestElementContainer.appendChild(this.container_);

    /** @private {!Element} */
    this.input_ = document.createElement(inputType);
    this.input_.onchange = () => {
      onChange(this.input_);
    };
    this.input_.id = shakaDemo.Input.generateNewId_('input');
    this.container_.appendChild(this.input_);

    if (parentContainer.latestTooltip) {
      // Since the row isn't focusable, add the tooltip information into the
      // accessibility data of the input, so it can be accessed by users using
      // screen readers.
      const extraInfo = document.createElement('span');
      extraInfo.textContent = parentContainer.latestTooltip;
      extraInfo.classList.add('hidden');
      extraInfo.id = shakaDemo.Input.generateNewId_('extra-info');
      this.container_.appendChild(extraInfo);
      this.input_.setAttribute('aria-describedby', extraInfo.id);
    }

    /**
     * Most MDL inputs require some sort of "sibling element" that exists at
     * the same level as the input itself. These other elements are used to
     * create various visual effects, such as the ripple effect.
     * @private {?Element}
     */
    this.extra_ = null;
    if (extraType) {
      this.extra_ = document.createElement(extraType);
      this.container_.appendChild(this.extra_);
    }
  }

  /** @return {!Element} */
  input() {
    return this.input_;
  }

  /** @return {!Element} */
  container() {
    return this.container_;
  }

  /** @return {?Element} */
  extra() {
    return this.extra_;
  }

  /**
  * @param {string} prefix
  * @return {string}
  * @private
  */
  static generateNewId_(prefix) {
    const idNumber = shakaDemo.Input.lastId_;
    shakaDemo.Input.lastId_ += 1;
    return prefix + '-labeled-' + idNumber;
  }
};

/** @private {number} */
shakaDemo.Input.lastId_ = 0;

/**
 * Creates and contains the MDL elements of a select input.
 */
shakaDemo.SelectInput = class extends shakaDemo.Input {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   * @param {!Object.<string, string>} values
   */
  constructor(parentContainer, name, onChange, values) {
    super(parentContainer, 'select', 'div', 'label', onChange);
    this.container_.classList.add('mdl-textfield');
    this.container_.classList.add('mdl-js-textfield');
    this.container_.classList.add('mdl-textfield--floating-label');
    this.input_.classList.add('mdl-textfield__input');
    this.extra_.classList.add('mdl-textfield__label');
    this.extra_.setAttribute('for', this.input_.id);
    for (const value of Object.keys(values)) {
      const option = document.createElement('option');
      option.textContent = values[value];
      option.value = value;
      this.input_.appendChild(option);
    }
  }
};

/**
 * Creates and contains the MDL elements of a bool input.
 */
shakaDemo.BoolInput = class extends shakaDemo.Input {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   */
  constructor(parentContainer, name, onChange) {
    super(parentContainer, 'input', 'label', 'span', onChange);
    this.input_.type = 'checkbox';
    this.container_.classList.add('mdl-switch');
    this.container_.classList.add('mdl-js-switch');
    this.container_.classList.add('mdl-js-ripple-effect');
    this.container_.setAttribute('for', this.input_.id);
    this.input_.classList.add('mdl-switch__input');
    this.extra_.classList.add('mdl-switch__label');
  }
};

/**
 * Creates and contains the MDL elements of a text input.
 */
shakaDemo.TextInput = class extends shakaDemo.Input {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   */
  constructor(parentContainer, name, onChange) {
    super(parentContainer, 'input', 'div', 'label', onChange);
    this.container_.classList.add('mdl-textfield');
    this.container_.classList.add('mdl-js-textfield');
    this.container_.classList.add('mdl-textfield--floating-label');
    this.input_.classList.add('mdl-textfield__input');
    this.extra_.classList.add('mdl-textfield__label');
    this.extra_.setAttribute('for', this.input_.id);
  }
};

/**
 * Creates and contains the MDL elements of a datalist input.
 */
shakaDemo.DatalistInput = class extends shakaDemo.TextInput {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   * @param {!Array.<string>} values
   */
  constructor(parentContainer, name, onChange, values) {
    super(parentContainer, name, onChange);
    // This element is not literally a datalist, as those are not supported on
    // all platforms (and they also have no MDL style support).
    // Instead, this is using the third-party "awesomplete" module, which acts
    // as a text field with autocomplete selection.
    const awesomplete = new Awesomplete(this.input_);
    awesomplete.list = values.slice(); // Make a local copy of the values list.
    awesomplete.minChars = 0;
    this.input_.addEventListener('focus', () => {
      // By default, awesomplete does not show suggestions on focusing on the
      // input, only on typing something.
      // This manually updates the suggestions, so that they will show up.
      awesomplete.evaluate();
    });
  }
};

/**
 * Creates and contains the MDL elements of a number input.
 */
shakaDemo.NumberInput = class extends shakaDemo.TextInput {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   * @param {boolean} canBeDecimal
   * @param {boolean} canBeZero
   * @param {boolean} canBeUnset
   */
  constructor(
      parentContainer, name, onChange, canBeDecimal, canBeZero, canBeUnset) {
    super(parentContainer, name, onChange);
    const error = document.createElement('span');
    error.classList.add('mdl-textfield__error');
    this.container_.appendChild(error);

    error.textContent = 'Must be a positive';
    this.input_.pattern = '(Infinity|';
    if (canBeZero) {
      this.input_.pattern += '[0-9]*';
    } else {
      this.input_.pattern += '[0-9]*[1-9][0-9]*';
      error.textContent += ', nonzero';
    }
    if (canBeDecimal) {
      // TODO: Handle commas as decimal delimeters, for appropriate regions?
      this.input_.pattern += '(.[0-9]+)?';
      error.textContent += ' number.';
    } else {
      error.textContent += ' integer.';
    }
    this.input_.pattern += ')';
    if (canBeUnset) {
      this.input_.pattern += '?';
    }
  }
};
<commit_msg>Fix datalist inputs in demo.<commit_after>/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shakaDemo.BoolInput');
goog.provide('shakaDemo.DatalistInput');
goog.provide('shakaDemo.Input');
goog.provide('shakaDemo.NumberInput');
goog.provide('shakaDemo.SelectInput');
goog.provide('shakaDemo.TextInput');

/**
 * Creates and contains the MDL elements of a type of input.
 */
shakaDemo.Input = class {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} inputType The element type for the input object.
   * @param {string} containerType The element type for the container containing
   *   the input object.
   * @param {string} extraType The element type for the "sibling element" to the
   *   input object. If null, it adds no such element.
   * @param {function(!Element)} onChange
   */
  constructor(parentContainer, inputType, containerType, extraType, onChange) {
    /** @private {!Element} */
    this.container_ = document.createElement(containerType);
    parentContainer.latestElementContainer.appendChild(this.container_);

    /** @private {!Element} */
    this.input_ = document.createElement(inputType);
    this.input_.onchange = () => {
      onChange(this.input_);
    };
    this.input_.id = shakaDemo.Input.generateNewId_('input');
    this.container_.appendChild(this.input_);

    if (parentContainer.latestTooltip) {
      // Since the row isn't focusable, add the tooltip information into the
      // accessibility data of the input, so it can be accessed by users using
      // screen readers.
      const extraInfo = document.createElement('span');
      extraInfo.textContent = parentContainer.latestTooltip;
      extraInfo.classList.add('hidden');
      extraInfo.id = shakaDemo.Input.generateNewId_('extra-info');
      this.container_.appendChild(extraInfo);
      this.input_.setAttribute('aria-describedby', extraInfo.id);
    }

    /**
     * Most MDL inputs require some sort of "sibling element" that exists at
     * the same level as the input itself. These other elements are used to
     * create various visual effects, such as the ripple effect.
     * @private {?Element}
     */
    this.extra_ = null;
    if (extraType) {
      this.extra_ = document.createElement(extraType);
      this.container_.appendChild(this.extra_);
    }
  }

  /** @return {!Element} */
  input() {
    return this.input_;
  }

  /** @return {!Element} */
  container() {
    return this.container_;
  }

  /** @return {?Element} */
  extra() {
    return this.extra_;
  }

  /**
  * @param {string} prefix
  * @return {string}
  * @private
  */
  static generateNewId_(prefix) {
    const idNumber = shakaDemo.Input.lastId_;
    shakaDemo.Input.lastId_ += 1;
    return prefix + '-labeled-' + idNumber;
  }
};

/** @private {number} */
shakaDemo.Input.lastId_ = 0;

/**
 * Creates and contains the MDL elements of a select input.
 */
shakaDemo.SelectInput = class extends shakaDemo.Input {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   * @param {!Object.<string, string>} values
   */
  constructor(parentContainer, name, onChange, values) {
    super(parentContainer, 'select', 'div', 'label', onChange);
    this.container_.classList.add('mdl-textfield');
    this.container_.classList.add('mdl-js-textfield');
    this.container_.classList.add('mdl-textfield--floating-label');
    this.input_.classList.add('mdl-textfield__input');
    this.extra_.classList.add('mdl-textfield__label');
    this.extra_.setAttribute('for', this.input_.id);
    for (const value of Object.keys(values)) {
      const option = document.createElement('option');
      option.textContent = values[value];
      option.value = value;
      this.input_.appendChild(option);
    }
  }
};

/**
 * Creates and contains the MDL elements of a bool input.
 */
shakaDemo.BoolInput = class extends shakaDemo.Input {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   */
  constructor(parentContainer, name, onChange) {
    super(parentContainer, 'input', 'label', 'span', onChange);
    this.input_.type = 'checkbox';
    this.container_.classList.add('mdl-switch');
    this.container_.classList.add('mdl-js-switch');
    this.container_.classList.add('mdl-js-ripple-effect');
    this.container_.setAttribute('for', this.input_.id);
    this.input_.classList.add('mdl-switch__input');
    this.extra_.classList.add('mdl-switch__label');
  }
};

/**
 * Creates and contains the MDL elements of a text input.
 */
shakaDemo.TextInput = class extends shakaDemo.Input {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   */
  constructor(parentContainer, name, onChange) {
    super(parentContainer, 'input', 'div', 'label', onChange);
    this.container_.classList.add('mdl-textfield');
    this.container_.classList.add('mdl-js-textfield');
    this.container_.classList.add('mdl-textfield--floating-label');
    this.input_.classList.add('mdl-textfield__input');
    this.extra_.classList.add('mdl-textfield__label');
    this.extra_.setAttribute('for', this.input_.id);
  }
};

/**
 * Creates and contains the MDL elements of a datalist input.
 */
shakaDemo.DatalistInput = class extends shakaDemo.TextInput {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   * @param {!Array.<string>} values
   */
  constructor(parentContainer, name, onChange, values) {
    super(parentContainer, name, onChange);
    // This element is not literally a datalist, as those are not supported on
    // all platforms (and they also have no MDL style support).
    // Instead, this is using the third-party "awesomplete" module, which acts
    // as a text field with autocomplete selection.
    const awesomplete = new Awesomplete(this.input_);
    awesomplete.list = values.slice(); // Make a local copy of the values list.
    awesomplete.minChars = 0;
    this.input_.addEventListener('focus', () => {
      // By default, awesomplete does not show suggestions on focusing on the
      // input, only on typing something.
      // This manually updates the suggestions, so that they will show up.
      awesomplete.evaluate();
    });
    this.input_.addEventListener('awesomplete-selectcomplete', () => {
      onChange(this.input_);
    });
  }
};

/**
 * Creates and contains the MDL elements of a number input.
 */
shakaDemo.NumberInput = class extends shakaDemo.TextInput {
  /**
   * @param {!shakaDemo.InputContainer} parentContainer
   * @param {string} name
   * @param {function(!Element)} onChange
   * @param {boolean} canBeDecimal
   * @param {boolean} canBeZero
   * @param {boolean} canBeUnset
   */
  constructor(
      parentContainer, name, onChange, canBeDecimal, canBeZero, canBeUnset) {
    super(parentContainer, name, onChange);
    const error = document.createElement('span');
    error.classList.add('mdl-textfield__error');
    this.container_.appendChild(error);

    error.textContent = 'Must be a positive';
    this.input_.pattern = '(Infinity|';
    if (canBeZero) {
      this.input_.pattern += '[0-9]*';
    } else {
      this.input_.pattern += '[0-9]*[1-9][0-9]*';
      error.textContent += ', nonzero';
    }
    if (canBeDecimal) {
      // TODO: Handle commas as decimal delimeters, for appropriate regions?
      this.input_.pattern += '(.[0-9]+)?';
      error.textContent += ' number.';
    } else {
      error.textContent += ' integer.';
    }
    this.input_.pattern += ')';
    if (canBeUnset) {
      this.input_.pattern += '?';
    }
  }
};
<|endoftext|>


<commit_before>var uuid      = require('uuid');
var diff      = require('deep-diff').diff;
var inspector = require('schema-inspector');
var utils = require('./utils');

var SCHEMAS = {
  addToBasket: {
    validation:   require('../schemas/validation/addToBasket.json'),
    sanitization: require('../schemas/sanitization/addToBasket.json')
  },
  navigation: {
    validation:   require('../schemas/validation/navigation.json'),
    sanitization: require('../schemas/sanitization/navigation.json')
  },
  order: {
    validation:   require('../schemas/validation/order.json'),
    sanitization: require('../schemas/sanitization/order.json')
  },
  autoSearch: {
    validation:   require('../schemas/validation/autoSearch.json'),
    sanitization: require('../schemas/sanitization/autoSearch.json')
  },
  search: {
    validation:   require('../schemas/validation/search.json'),
    sanitization: require('../schemas/sanitization/search.json')
  },
  sessionChange: {
    validation:   require('../schemas/validation/sessionChange.json'),
    sanitization: require('../schemas/sanitization/sessionChange.json')
  },
  viewProduct: {
    validation:   require('../schemas/validation/viewProduct.json'),
    sanitization: require('../schemas/sanitization/viewProduct.json')
  }
};

// Info on path length limitations: http://stackoverflow.com/a/812962
var MAX_IE_PATH_LENGTH    = 2048; // Thanks IE
var MAX_OTHER_PATH_LENGTH = 80000; // Thanks Microsoft Edge
var MAX_PATHNAME_LENGTH   = 100; // '/v2/pixel/?random=0.5405421565044588&m=' plus extra for luck
var MAX_SEGMENT_COUNT     = 100;

var overridenPixelPath = null;

var Tracker = function (customerId, area) {
  var self                 = this;
  var customer             = {};
  var visit                = {customerData: {}};
  var invalidEventCallback = null;

  var MAX_PATH_LENGTH         = (utils.getInternetExplorerVersion(navigator.appCodeName, navigator.userAgent) > -1) ? MAX_IE_PATH_LENGTH : MAX_OTHER_PATH_LENGTH;
  var MAX_QUERY_STRING_LENGTH = MAX_PATH_LENGTH - MAX_PATHNAME_LENGTH;

  if (typeof customerId !== 'string' || customerId.length === 0) {
    throw new Error('customerId must be a string with length');
  } else {
    customer.id = customerId;
  }

  if (typeof area === 'string' && area.length > 0) {
    customer.area = area;
  }

  /**
   * Update visitor and session id's during login/logout
   * @param visitorId
   * @param sessionId
   */
  self.setVisitor = function (visitorId, sessionId) {
    visitorId = (visitorId && typeof visitorId === 'number') ? (visitorId + '') : visitorId;
    sessionId = (sessionId && typeof sessionId === 'number') ? (sessionId + '') : sessionId;

    if (typeof visitorId !== 'string') {
      throw new Error('visitorId must be a string with length');
    }

    if (typeof sessionId !== 'string') {
      throw new Error('sessionId must be a string with length');
    }

    var prevVisitorId = visit.customerData.visitorId;
    var prevSessionId = visit.customerData.sessionId;

    visit.customerData.visitorId = visitorId;
    visit.customerData.sessionId = sessionId;

    if (prevVisitorId !== visitorId || prevSessionId !== sessionId) {
      var sessionEvent = {
        newSessionId: visit.customerData.sessionId,
        newVisitorId: visit.customerData.visitorId
      };

      // There may not be a previous session (initial site load)
      if (prevVisitorId) {
        sessionEvent.previousVisitorId = prevVisitorId;
        sessionEvent.previousSessionId = prevSessionId;
      }

      self.__private.sendSessionChangeEvent({session: sessionEvent});
    }
  };

  /**
   * Append eventType, customer, and visit to event
   * @param event
   * @param type
   */
  var prepareEvent = function (event, type) {
    if (!visit.customerData.sessionId || !visit.customerData.visitorId) {
      throw new Error('visitorId and sessionId must be set using setVisitor() before an event is sent');
    }

    event.eventType = type;
    event.customer = customer;
    event.visit = visit;
    return event;
  };

  /**
   * Set callback to be notified of invalid events
   * @param callback
   */
  self.setInvalidEventCallback = function (callback) {
    if (typeof callback !== 'function') {
      throw new Error('invalid event callback must be a function');
    }

    invalidEventCallback = callback;
  };

  /**
   * Validate and send addToBasket event
   * @param event
   */
  self.sendAddToBasketEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'addToBasket');
  };

  /**
   * Validate and send order event
   * @param event
   */
  self.sendOrderEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'order');
  };

  /**
   * Validate and send search event
   * @param event
   */
  self.sendSearchEvent = function (event) {
    if (event.autoId) {
      self.__private.prepareAndSendEvent(event, 'autoSearch');
    } else {
      self.__private.prepareAndSendEvent(event, 'search');
    }
  };

  /**
   * Validate and send viewProduct event
   * @param event
   */
  self.sendViewProductEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'viewProduct');
  };

  self.__private = {};

  /**
   * Helper to prepare, validate, and send event
   * @param event
   * @param eventType
   */
  self.__private.prepareAndSendEvent = function (event, eventType) {
    event = prepareEvent(event, eventType);
    var validated = self.__private.validateEvent(event, SCHEMAS[eventType]);
    if (validated && validated.event) {
      self.__private.sendEvent(validated.event, self.__private.sendSegment);
    } else {
      invalidEventCallback && invalidEventCallback(event, validated.error);
    }
  };

  /**
   * Visitor getter for testing
   * @returns {{customerData: {}}}
   */
  self.__private.getVisitor = function () {
    return visit;
  };

  /**
   * Attach navigation listeners for page enter/leave and hash changes
   */
  self.__private.attachNavigationListeners = function () {
    if (!window || !document) {
      console.warn('no window or document object found, cannot attach navigation listeners');
      return;
    }

    // Mozilla, Opera, Webkit
    if (document.addEventListener) {
      document.addEventListener('DOMContentLoaded', function () {
        document.removeEventListener('DOMContentLoaded', arguments.callee, false);
        self.__private.sendNavigationEvent({
          navigation: {
            type:       'enter',
            exactEvent: 'DOMContentLoaded'
          }
        });
      }, false);

      // If IE event model is used
    } else if (document.attachEvent) {
      // ensure firing before onload
      document.attachEvent('onreadystatechange', function () {
        if (document.readyState === 'complete') {
          document.detachEvent('onreadystatechange', arguments.callee);
          self.__private.sendNavigationEvent({
            navigation: {
              type:       'enter',
              exactEvent: 'onreadystatechange'
            }
          });
        }
      });
    }

    // exit if the browser implements that event
    if (window.addEventListener) {
      window.addEventListener('hashchange', function () {
        self.__private.sendNavigationEvent({
          navigation: {
            type:       'hashchange',
            exactEvent: 'hashchange'
          }
        });
      });
    }
  };

  self.__private.attachNavigationListeners();

  /**
   * Validate and send navigation event
   * @param event
   */
  self.__private.sendNavigationEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'navigation');
  };

  /**
   * Validate and send session change event
   * @param event
   */
  self.__private.sendSessionChangeEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'sessionChange');
  };

  /**
   * Based on the schema provided, validate an event for sending to the tracker endpoint
   * @param event
   * @param schemas
   */
  self.__private.validateEvent = function (event, schemas) {
    var sanitizedEvent = utils.deepCopy(event);
    inspector.sanitize(schemas.sanitization, sanitizedEvent);
    var result = inspector.validate(schemas.validation, sanitizedEvent);

    if (!result.valid) {
      console.error('error while processing event: ' + result.format());
      return {event: null, error: result.format()};
    }

    if (!sanitizedEvent.visit) {
      sanitizedEvent.visit = {};
    }

    if (!sanitizedEvent.visit.generated) {
      sanitizedEvent.visit.generated = {};
    }

    var removedFields = self.__private.getRemovedFields(sanitizedEvent, event);

    if (removedFields.length > 0) {
      for (var i = 0; i < removedFields.length; i++) {
        console.warn('unexpected field: ' + removedFields[i] + ' is being dropped from eventType: ' + sanitizedEvent.eventType);
      }
    }

    sanitizedEvent.visit.generated.uri = (typeof window !== 'undefined' && window.location) ? window.location.href : '';
    sanitizedEvent.visit.generated.timezoneOffset = new Date().getTimezoneOffset();
    sanitizedEvent.visit.generated.localTime = new Date().toISOString();

    return {event: sanitizedEvent};
  };

  /**
   * Compared the sanitized event to the original, and return an object containing the properties that were removed.
   * @param sanitizedEvent
   * @param originalEvent
   * @returns {*}
   */
  self.__private.getRemovedFields = function (sanitizedEvent, originalEvent) {
    var allDifferences = diff(sanitizedEvent, originalEvent);
    var removedFields  = [];

    for (var i = 0; i < allDifferences.length; i++) {
      if (allDifferences[i].kind === 'N') {
        removedFields.push(allDifferences[i].path.join('.'));
      }
    }

    return removedFields;
  };

  /**
   * Take event, convert to string, split by max length, and send along with uuid and customer info
   * @param event
   * @param sendSegment
   */
  self.__private.sendEvent = function (event, sendSegment) {
    var eventString = JSON.stringify(event);
    var uuidString  = uuid.v4();

    var segmentTemplate = {
      uuid:     uuidString,
      id:       MAX_SEGMENT_COUNT,
      total:    MAX_SEGMENT_COUNT,
      customer: event.customer
    };

    var SEGMENT_WRAPPER_OVERHEAD = encodeURIComponent(JSON.stringify(segmentTemplate)).length;
    var eventStringSegments      = utils.chunkString(encodeURIComponent(eventString), MAX_QUERY_STRING_LENGTH - SEGMENT_WRAPPER_OVERHEAD);

    if (eventStringSegments.length > MAX_SEGMENT_COUNT) {
      console.error('cannot send: ' + eventStringSegments + ' event segments, as that exceeds the max of: ' + MAX_SEGMENT_COUNT);
      return;
    }

    for (var i = 0; i < eventStringSegments.length; i++) {
      sendSegment({
        uuid:     uuidString,
        segment:  decodeURIComponent(eventStringSegments[i]), // To prevent double-encoding, it'll be re-encoded before sending
        id:       i,
        total:    eventStringSegments.length,
        customer: event.customer
      });
    }
  };

  /**
   * Use pixel endpoint to upload string to event-tracker
   * @param segment
   */
  self.__private.sendSegment = function (segment) {
    var protocol = 'https:' == document.location.protocol ? 'https://tracker-secure' : 'http://tracker';
    var host     = protocol + '.groupbycloud.com';
    var params   = '?random\x3d' + Math.random(); // To bust the cache
    params += '&m=' + encodeURIComponent(JSON.stringify(segment));

    var path = '/v2/pixel/' + params;

    if (path.length > MAX_PATH_LENGTH) {
      console.error('cannot send request with path exceeding max length of: ' + MAX_PATH_LENGTH + ' path is: ' + path.length);
      return;
    }

    var im = new Image();
    if (!overridenPixelPath) {
      im.src = host + path;
    } else {
      im.src = overridenPixelPath + params;
    }
  };

};

Tracker.__overridePixelPath = function (path) {
  overridenPixelPath = path;
};

module.exports = Tracker;<commit_msg>Fix schema require<commit_after>var uuid      = require('uuid');
var diff      = require('deep-diff').diff;
var inspector = require('schema-inspector');
var utils     = require('./utils');

var SCHEMAS = {
  addToBasket:   require('../schemas/addToBasket'),
  navigation:    require('../schemas/navigation'),
  order:         require('../schemas/order'),
  autoSearch:    require('../schemas/autoSearch'),
  search:        require('../schemas/search'),
  sessionChange: require('../schemas/sessionChange'),
  viewProduct:   require('../schemas/viewProduct')
};

// Info on path length limitations: http://stackoverflow.com/a/812962
var MAX_IE_PATH_LENGTH    = 2048; // Thanks IE
var MAX_OTHER_PATH_LENGTH = 80000; // Thanks Microsoft Edge
var MAX_PATHNAME_LENGTH   = 100; // '/v2/pixel/?random=0.5405421565044588&m=' plus extra for luck
var MAX_SEGMENT_COUNT     = 100;

var overridenPixelPath = null;

var Tracker = function (customerId, area) {
  var self                 = this;
  var customer             = {};
  var visit                = {customerData: {}};
  var invalidEventCallback = null;

  var MAX_PATH_LENGTH         = (utils.getInternetExplorerVersion(navigator.appCodeName, navigator.userAgent) > -1) ? MAX_IE_PATH_LENGTH : MAX_OTHER_PATH_LENGTH;
  var MAX_QUERY_STRING_LENGTH = MAX_PATH_LENGTH - MAX_PATHNAME_LENGTH;

  if (typeof customerId !== 'string' || customerId.length === 0) {
    throw new Error('customerId must be a string with length');
  } else {
    customer.id = customerId;
  }

  if (typeof area === 'string' && area.length > 0) {
    customer.area = area;
  }

  /**
   * Update visitor and session id's during login/logout
   * @param visitorId
   * @param sessionId
   */
  self.setVisitor = function (visitorId, sessionId) {
    visitorId = (visitorId && typeof visitorId === 'number') ? (visitorId + '') : visitorId;
    sessionId = (sessionId && typeof sessionId === 'number') ? (sessionId + '') : sessionId;

    if (typeof visitorId !== 'string') {
      throw new Error('visitorId must be a string with length');
    }

    if (typeof sessionId !== 'string') {
      throw new Error('sessionId must be a string with length');
    }

    var prevVisitorId = visit.customerData.visitorId;
    var prevSessionId = visit.customerData.sessionId;

    visit.customerData.visitorId = visitorId;
    visit.customerData.sessionId = sessionId;

    if (prevVisitorId !== visitorId || prevSessionId !== sessionId) {
      var sessionEvent = {
        newSessionId: visit.customerData.sessionId,
        newVisitorId: visit.customerData.visitorId
      };

      // There may not be a previous session (initial site load)
      if (prevVisitorId) {
        sessionEvent.previousVisitorId = prevVisitorId;
        sessionEvent.previousSessionId = prevSessionId;
      }

      self.__private.sendSessionChangeEvent({session: sessionEvent});
    }
  };

  /**
   * Append eventType, customer, and visit to event
   * @param event
   * @param type
   */
  var prepareEvent = function (event, type) {
    if (!visit.customerData.sessionId || !visit.customerData.visitorId) {
      throw new Error('visitorId and sessionId must be set using setVisitor() before an event is sent');
    }

    event.eventType = type;
    event.customer  = customer;
    event.visit     = visit;
    return event;
  };

  /**
   * Set callback to be notified of invalid events
   * @param callback
   */
  self.setInvalidEventCallback = function (callback) {
    if (typeof callback !== 'function') {
      throw new Error('invalid event callback must be a function');
    }

    invalidEventCallback = callback;
  };

  /**
   * Validate and send addToBasket event
   * @param event
   */
  self.sendAddToBasketEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'addToBasket');
  };

  /**
   * Validate and send order event
   * @param event
   */
  self.sendOrderEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'order');
  };

  /**
   * Validate and send search event
   * @param event
   */
  self.sendSearchEvent = function (event) {
    if (event.autoId) {
      self.__private.prepareAndSendEvent(event, 'autoSearch');
    } else {
      self.__private.prepareAndSendEvent(event, 'search');
    }
  };

  /**
   * Validate and send viewProduct event
   * @param event
   */
  self.sendViewProductEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'viewProduct');
  };

  self.__private = {};

  /**
   * Helper to prepare, validate, and send event
   * @param event
   * @param eventType
   */
  self.__private.prepareAndSendEvent = function (event, eventType) {
    event         = prepareEvent(event, eventType);
    var validated = self.__private.validateEvent(event, SCHEMAS[eventType]);
    if (validated && validated.event) {
      self.__private.sendEvent(validated.event, self.__private.sendSegment);
    } else {
      invalidEventCallback && invalidEventCallback(event, validated.error);
    }
  };

  /**
   * Visitor getter for testing
   * @returns {{customerData: {}}}
   */
  self.__private.getVisitor = function () {
    return visit;
  };

  /**
   * Attach navigation listeners for page enter/leave and hash changes
   */
  self.__private.attachNavigationListeners = function () {
    if (!window || !document) {
      console.warn('no window or document object found, cannot attach navigation listeners');
      return;
    }

    // Mozilla, Opera, Webkit
    if (document.addEventListener) {
      document.addEventListener('DOMContentLoaded', function () {
        document.removeEventListener('DOMContentLoaded', arguments.callee, false);
        self.__private.sendNavigationEvent({
          navigation: {
            type:       'enter',
            exactEvent: 'DOMContentLoaded'
          }
        });
      }, false);

      // If IE event model is used
    } else if (document.attachEvent) {
      // ensure firing before onload
      document.attachEvent('onreadystatechange', function () {
        if (document.readyState === 'complete') {
          document.detachEvent('onreadystatechange', arguments.callee);
          self.__private.sendNavigationEvent({
            navigation: {
              type:       'enter',
              exactEvent: 'onreadystatechange'
            }
          });
        }
      });
    }

    // exit if the browser implements that event
    if (window.addEventListener) {
      window.addEventListener('hashchange', function () {
        self.__private.sendNavigationEvent({
          navigation: {
            type:       'hashchange',
            exactEvent: 'hashchange'
          }
        });
      });
    }
  };

  self.__private.attachNavigationListeners();

  /**
   * Validate and send navigation event
   * @param event
   */
  self.__private.sendNavigationEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'navigation');
  };

  /**
   * Validate and send session change event
   * @param event
   */
  self.__private.sendSessionChangeEvent = function (event) {
    self.__private.prepareAndSendEvent(event, 'sessionChange');
  };

  /**
   * Based on the schema provided, validate an event for sending to the tracker endpoint
   * @param event
   * @param schemas
   */
  self.__private.validateEvent = function (event, schemas) {
    var sanitizedEvent = utils.deepCopy(event);
    inspector.sanitize(schemas.sanitization, sanitizedEvent);
    var result = inspector.validate(schemas.validation, sanitizedEvent);

    if (!result.valid) {
      console.error('error while processing event: ' + result.format());
      return {
        event: null,
        error: result.format()
      };
    }

    if (!sanitizedEvent.visit) {
      sanitizedEvent.visit = {};
    }

    if (!sanitizedEvent.visit.generated) {
      sanitizedEvent.visit.generated = {};
    }

    var removedFields = self.__private.getRemovedFields(sanitizedEvent, event);

    if (removedFields.length > 0) {
      for (var i = 0; i < removedFields.length; i++) {
        console.warn('unexpected field: ' + removedFields[i] + ' is being dropped from eventType: ' + sanitizedEvent.eventType);
      }
    }

    sanitizedEvent.visit.generated.uri            = (typeof window !== 'undefined' && window.location) ? window.location.href : '';
    sanitizedEvent.visit.generated.timezoneOffset = new Date().getTimezoneOffset();
    sanitizedEvent.visit.generated.localTime      = new Date().toISOString();

    return {event: sanitizedEvent};
  };

  /**
   * Compared the sanitized event to the original, and return an object containing the properties that were removed.
   * @param sanitizedEvent
   * @param originalEvent
   * @returns {*}
   */
  self.__private.getRemovedFields = function (sanitizedEvent, originalEvent) {
    var allDifferences = diff(sanitizedEvent, originalEvent);
    var removedFields  = [];

    for (var i = 0; i < allDifferences.length; i++) {
      if (allDifferences[i].kind === 'N') {
        removedFields.push(allDifferences[i].path.join('.'));
      }
    }

    return removedFields;
  };

  /**
   * Take event, convert to string, split by max length, and send along with uuid and customer info
   * @param event
   * @param sendSegment
   */
  self.__private.sendEvent = function (event, sendSegment) {
    var eventString = JSON.stringify(event);
    var uuidString  = uuid.v4();

    var segmentTemplate = {
      uuid:     uuidString,
      id:       MAX_SEGMENT_COUNT,
      total:    MAX_SEGMENT_COUNT,
      customer: event.customer
    };

    var SEGMENT_WRAPPER_OVERHEAD = encodeURIComponent(JSON.stringify(segmentTemplate)).length;
    var eventStringSegments      = utils.chunkString(encodeURIComponent(eventString), MAX_QUERY_STRING_LENGTH - SEGMENT_WRAPPER_OVERHEAD);

    if (eventStringSegments.length > MAX_SEGMENT_COUNT) {
      console.error('cannot send: ' + eventStringSegments + ' event segments, as that exceeds the max of: ' + MAX_SEGMENT_COUNT);
      return;
    }

    for (var i = 0; i < eventStringSegments.length; i++) {
      sendSegment({
        uuid:     uuidString,
        segment:  decodeURIComponent(eventStringSegments[i]), // To prevent double-encoding, it'll be re-encoded before sending
        id:       i,
        total:    eventStringSegments.length,
        customer: event.customer
      });
    }
  };

  /**
   * Use pixel endpoint to upload string to event-tracker
   * @param segment
   */
  self.__private.sendSegment = function (segment) {
    var protocol = 'https:' == document.location.protocol ? 'https://tracker-secure' : 'http://tracker';
    var host     = protocol + '.groupbycloud.com';
    var params   = '?random\x3d' + Math.random(); // To bust the cache
    params += '&m=' + encodeURIComponent(JSON.stringify(segment));

    var path = '/v2/pixel/' + params;

    if (path.length > MAX_PATH_LENGTH) {
      console.error('cannot send request with path exceeding max length of: ' + MAX_PATH_LENGTH + ' path is: ' + path.length);
      return;
    }

    var im = new Image();
    if (!overridenPixelPath) {
      im.src = host + path;
    } else {
      im.src = overridenPixelPath + params;
    }
  };

};

Tracker.__overridePixelPath = function (path) {
  overridenPixelPath = path;
};

module.exports = Tracker;<|endoftext|>


<commit_before>/**
 * A HTML5 Validation replacement
 */
void function (document, window, $) {
    "use strict";

    var ERROR_CLASS_NAME = 'form-error', INPUT_ERROR_CLASS = 'invalid', LOADING_CLASS = 'loading',
        ARIA_INVALID = 'aria-invalid', REMOTE = 'data-remote', REMOTE_ACTION = 'data-remote-action';
    if (!window.FlexCss) {
        window.FlexCss = {};
    }

    var FlexCss = window.FlexCss;

    /**
     * jQuery helper to serialize a form to json
     * @returns {{}}
     */
    $.fn.serializeFormJSON = function () {

        var o = {};
        var a = this.serializeArray();
        $.each(a, function () {
            if (o[this.name]) {
                if (!o[this.name].push) {
                    o[this.name] = [o[this.name]];
                }
                o[this.name].push(this.value || '');
            } else {
                o[this.name] = this.value || '';
            }
        });
        return o;
    };

    /**
     * Enhanced flexcss forms
     * @param {HTMLElement} form
     * @param {Object} options
     * @constructor
     */
    FlexCss.Form = function (form, options) {

        var self = this;

        self.tooltips = null;

        self.options = {
            createTooltips: true,
            appendError: false,
            ajaxSubmitType: 'POST',
            ajaxJsonContentType: 'application/json; charset=utf-8'
        };

        self.options = $.extend(self.options, options);

        // set form class as widget
        // Forms are very different to classical widgets,
        // we will not use our base widget class for this but just self
        self.hfWidgetInstance = self;

        /**
         * Submits this form, either via ajax or just classical (default)
         * @param {HTMLElement} thisForm
         * @param {Event} e
         * @returns {*}
         */
        self.submitFunction = function (thisForm, e) {
            var shouldUseAjax = thisForm.getAttribute(REMOTE), ajaxPostUrl =
                    thisForm.getAttribute(REMOTE_ACTION) ||
                    thisForm.getAttribute('action') || window.location.href,
                useJson = 'json' === shouldUseAjax;
            thisForm.classList.add(LOADING_CLASS);

            $(thisForm).trigger('flexcss.form.submit', [e, self, thisForm]);

            if (null === shouldUseAjax) {
                return thisForm.submit();
            }

            // prevent form from submit normally
            e.preventDefault();

            // support either JSON request payload or normal payload submission
            var serverCall = useJson ? $.ajax({
                contentType: self.options.ajaxJsonContentType,
                type: self.options.ajaxSubmitType,
                url: ajaxPostUrl,
                data: JSON.stringify($(thisForm).serializeFormJSON())
            }) : $.ajax({
                type: self.options.ajaxSubmitType,
                url: ajaxPostUrl,
                data: $(thisForm).serialize()
            });

            serverCall.then(function (r) {
                (self._remoteValidationFunction || FlexCss.Form.globalRemoteValidationFunction).apply(self, [r]);
                return $.Deferred().resolve(r);
            }).always(function (r) {
                $(thisForm).trigger('flexcss.form.ajaxCompleted', [e,self, thisForm, r]);
                // always remove error class
                thisForm.classList.remove(LOADING_CLASS);
            });
        };

        /**
         * This form
         * @returns {HTMLElement}
         */
        self.getForm = function () {
            return form;
        };

        /**
         * A List of Validators
         * @type {Object}
         * @private
         */
        self._validators = FlexCss.Form.globalValidators;

        /**
         * @type {Function}
         * @private
         */
        self._remoteValidationFunction = null;

        /**
         * Registers a function that handles remote validation
         * @param {Function} func
         * @returns {window.FlexCss.Form}
         */
        self.registerRemoteValidation = function (func) {
            self._remoteValidationFunction = func;
            return self;
        };

        /**
         * @param {HTMLElement} field
         * @param {ValidityState} validity
         * @returns {*}
         * @private
         */
        function _setupErrorMessages(field, validity) {
            return FlexCss.Form.globalErrorMessageHandler ?
                FlexCss.Form.globalErrorMessageHandler.apply(self, [field, validity]) : false;
        }

        /**
         * @param {HTMLElement} thisParent
         * @private
         */
        function _removeElementErrors(thisParent) {
            var errors = thisParent.querySelectorAll('.' + ERROR_CLASS_NAME), inputsWithErrorClasses =
                thisParent.querySelectorAll('.' + INPUT_ERROR_CLASS);
            for (var elementErrorIndex = 0; elementErrorIndex < errors.length; elementErrorIndex++) {
                errors[elementErrorIndex].parentNode.removeChild(errors[elementErrorIndex]);
            }
            for (var inputErrorIndex = 0; inputErrorIndex < inputsWithErrorClasses.length; inputErrorIndex++) {
                inputsWithErrorClasses[inputErrorIndex].classList.remove(INPUT_ERROR_CLASS);
                if (self.tooltips) {
                    self.tooltips.removeTooltip(inputsWithErrorClasses[inputErrorIndex]);
                }
            }
        }

        /**
         * Registers a custom validator
         * @param {String} name
         * @param {Function} validator a validation function should always return either a Future(true) or Future(false)
         * even when the field has been invalidated with `setCustomValidity`, because of different browser bugs
         * we can't rely on that
         * @returns {window.FlexCss.Form}
         */
        self.registerValidator = function (name, validator) {
            self._validators[name] = validator;
            return self;
        };

        /**
         * Runs async validation
         * @param {String} validationRef
         * @param {HTMLElement} field
         * @returns {*}
         * @private
         */
        function _runValidation(validationRef, field) {
            if (!self._validators[validationRef]) {
                throw 'Could not found validator: ' + validationRef;
            }
            var cl = field.classList, future = self._validators[validationRef].apply(self, [field, form]);
            cl.add(LOADING_CLASS);
            future.always(function () {
                cl.remove(LOADING_CLASS);
            });
            return future;
        }

        /**
         * Run custom validations for elements, validations are done async do support XHR Requests or other stuff
         *
         * @param {Array|NodeList} fields
         * @returns {$.Deferred} contains either true if validations passed or false if something went wrong
         * @private
         */
        function _customValidationsForElements(fields) {
            var futures = [], fieldsLength = fields.length, checkedFields = [];
            for (var iVal = 0; iVal < fieldsLength; iVal++) {
                var field = fields[iVal], validationRef = field.getAttribute('data-validate'), validity = field.validity;
                if (self._validators[validationRef]) {
                    // use local validation first and then continue with custom validations
                    if (!validity.customError && !validity.valid) {
                        continue;
                    }
                    checkedFields.push(field);
                    futures.push(_runValidation(validationRef, field));
                }
            }
            return $.when.apply(self, futures).then(function () {
                var allFutures = arguments, l = allFutures.length;
                var result = {
                    checkedFields: checkedFields,
                    foundAnyError: false
                };

                for (var fI = 0; fI < l; fI++) {
                    if (!allFutures[fI]) {
                        result.foundAnyError = true;
                    }
                }
                return $.Deferred().resolve(result);
            });
        }

        /**
         * Will handle errors for given fields
         * @param {Array|NodeList} fields
         * @param {Boolean} removeAllErrors
         */
        self.prepareErrors = function (fields, removeAllErrors) {
            if (removeAllErrors) {
                _removeElementErrors(form);
            }
            // We save all validations in an extra property because we need to reset the validity due some
            // implementation errors in other browsers then chrome
            for (var i = 0; i < fields.length; i++) {
                var field = fields[i], parent = field.parentNode, validity = field.validity;
                field.flexFormsSavedValidity = JSON.parse(JSON.stringify(validity));
                if (validity && !validity.valid) {
                    if (!removeAllErrors) {
                        // Remove current errors:
                        _removeElementErrors(parent);
                    }
                    // setup custom error messages:
                    _setupErrorMessages(field, validity);
                    field.classList.add('invalid');
                    field.setAttribute(ARIA_INVALID, 'true');
                    if (self.options.appendError) {
                        parent.insertAdjacentHTML("beforeend", '
' +
                        field.validationMessage +
                        "
");
                    }
                    field.flexFormsSavedValidationMessage = field.validationMessage;
                } else {
                    field.classList.remove('invalid');
                    field.setAttribute(ARIA_INVALID, 'false');

                    _removeElementErrors(parent);
                }
                // FIXME: We have to reset the custom validity here to allow native validations work again
                field.setCustomValidity('');
            }
        };

        function validateCustomFields() {
            return _customValidationsForElements(
                form.querySelectorAll("[data-validate]"));
        }

        /**
         * Creates an array from a node list with invalid items
         * On Firefox also Fieldset's seems to be invalid, remove them
         * @param list
         * @returns {Array}
         * @private
         */
        function _createArrayFromInvalidFieldList(list) {
            var arr = [];
            for (var i = 0; i < list.length; ++i) {
                var n = list[i];
                if (!(n instanceof HTMLFieldSetElement)) {
                    arr.push(n);
                }
            }
            return arr;
        }

        /**
         * Formats the error content for the tooltip
         * @param {String} error
         * @returns {String}
         * @private
         */
        function _formatErrorTooltip(error) {
            return ' ' + error;
        }

        /**
         * Creates a tooltip at given element, will create a new instance if not created
         * @param {HTMLElement} target
         * @param {Boolean} [remove]
         * @private
         */
        function _showAndOrCreateTooltip(target, remove) {
            if (!self.tooltips && self.options.createTooltips) {
                self.tooltips = new FlexCss.Tooltip(form, {
                    containerClass: 'error-tooltip'
                });
            }

            if(!self.options.createTooltips) {
                return;
            }

            setTimeout(function () {
                if (!target.flexFormsSavedValidity) {
                    return;
                }
                if (!target.flexFormsSavedValidity.valid && target.classList.contains(INPUT_ERROR_CLASS)) {
                        self.tooltips.createTooltip(target,
                            _formatErrorTooltip(target.flexFormsSavedValidationMessage), false);
                } else {
                    if (remove) {
                            self.tooltips.removeTooltip(target);
                    }
                }
            }, 0);

        }

        self.showAndOrCreateTooltip = _showAndOrCreateTooltip;

        /**
         * Initializes validation for a given form, registers event handlers
         */
        function initFormValidation() {
            // Suppress the default bubbles
            var invalidFormFired = false, currentValidationFuture;
            form.addEventListener("invalid", function (e) {
                e.preventDefault();
                var invalidFields = form.querySelectorAll(":invalid");
                var arr = _createArrayFromInvalidFieldList(invalidFields);
                // Prevent fire this N times:
                if (arr.indexOf(e.target) > 0) {
                    return;
                }
                // focus the first field:
                if (arr.length > 0) {
                    setTimeout(function () {
                        arr[0].focus();
                        _showAndOrCreateTooltip(arr[0]);
                    }, 0);
                }
                currentValidationFuture = $.Deferred();

                var validation = validateCustomFields();
                self.prepareErrors(arr, true);
                validation.done(function (r) {
                    self.prepareErrors(r.checkedFields, false);
                    currentValidationFuture.resolve(r);
                    invalidFormFired = false;
                });

            }, true);

            // handle focus out for text elements
            // Will show an error if field was invalid the first time
            form.addEventListener("blur", function (e) {
                if (self.tooltips) {
                    self.tooltips.removeTooltip(e.target);
                }
                var target = e.target, hasError = false;

                if (!_checkIsValidBlurFocusElement(target)) {
                    return;
                }

                if (target.classList.contains(INPUT_ERROR_CLASS)) {
                    hasError = true;
                }
                _customValidationsForElements([e.target]).done(function () {
                    self.prepareErrors([e.target], false);
                    if (!hasError) {
                        _showAndOrCreateTooltip(e.target);
                    }
                });

            }, true);

            /**
             * Validates if target is a valid input field to check blur and focus events
             * @param {HTMLElement} target
             * @returns {boolean}
             */
            function _checkIsValidBlurFocusElement(target) {
                var attr = target.getAttribute('type');
                return !((attr === 'checkbox' || attr === 'option' || attr === 'submit' || !(target instanceof HTMLSelectElement || target instanceof HTMLInputElement ||
                target instanceof HTMLTextAreaElement)));
            }

            // handle focus on input elements
            // will show an error if field is invalid
            form.addEventListener("focus", function (e) {
                // do not track errors for checkbox and radios on focus:
                if (!_checkIsValidBlurFocusElement(e.target)) {
                    return;
                }
                _showAndOrCreateTooltip(e.target);
            }, true);

            // Handle change for checkbox, radios and selects
            form.addEventListener("change", function (e) {
                var name = e.target.getAttribute('name');
                if (name) {
                    var inputs = form.querySelectorAll('[name="' + name + '"]');
                    _customValidationsForElements(inputs).done(function () {
                        self.prepareErrors(inputs, false);
                        _showAndOrCreateTooltip(e.target, true);
                    });
                }
            });

            // prevent default if form is invalid
            var submitListener = function (e) {

                if (form.classList.contains(LOADING_CLASS)) {
                    e.preventDefault();
                    return false;
                }

                form.classList.add(LOADING_CLASS);
                form.removeEventListener("submit", submitListener);
                _removeElementErrors(form);
                e.preventDefault();
                // reset:
                if (form.checkValidity()) {
                    form.addEventListener("submit", submitListener);
                    // Custom validations did never pass
                    currentValidationFuture = $.Deferred();
                    var validation = validateCustomFields();
                    validation.done(function (r) {
                        // focus first invalid field:
                        for (var i = 0; i < r.checkedFields.length; i++) {
                            var f = r.checkedFields[i];
                            if (!f.validity.valid) {
                                f.focus();
                                break;
                            }
                        }
                        self.prepareErrors(r.checkedFields, false);
                        currentValidationFuture.resolve(r);
                    });
                    currentValidationFuture.done(function (r) {
                        form.classList.remove(LOADING_CLASS);
                        if (!r.foundAnyError) {
                            // Handle submitting the form to server:
                            self._handleSubmit(e);
                        }
                    });
                } else {
                    form.classList.remove(LOADING_CLASS);
                    form.addEventListener("submit", submitListener);
                }
            };
            form.addEventListener("submit", submitListener);
        }

        initFormValidation();

        /**
         * Handles submitting, optionally allows to stop submitting
         * @param e
         * @private
         */
        self._handleSubmit = function (e) {
            var obj = {
                abort: false
            };
            $(form).trigger('flexcss.form.beforeSubmit', [e, self, form, obj]);
            if (obj.abort === true) {
                return false;
            }
            self.submitFunction.apply(self, [form, e]);
        };

    };

    FlexCss.Form.globalErrorMessageHandler = function () {
    };

    /**
     * Registers a global event Handler
     * @param errorFunc
     */
    FlexCss.Form.registerErrorMessageHandler = function (errorFunc) {
        FlexCss.Form.globalErrorMessageHandler = errorFunc;
    };

    /**
     * Initialize forms for a specific selector
     * @param {String} selector
     * @param {Object} options
     */
    FlexCss.Form.init = function (selector, options) {
        var forms = selector instanceof HTMLElement ? selector.querySelectorAll('form') : document.querySelectorAll(selector);
        for (var i = 0; i < forms.length; i++) {
            new FlexCss.Form(forms[i], options);
        }
    };

    FlexCss.Form.globalValidators = [];
    FlexCss.Form.globalRemoteValidationFunction = function () {
    };

    /**
     * Registers a global validator that is usable on all form instasnces
     * @param {String} name
     * @param {Function} validator
     * @returns {Function}
     */
    FlexCss.Form.registerValidator = function (name, validator) {
        FlexCss.Form.globalValidators[name] = validator;
        return FlexCss.Form;
    };

    /**
     * Registers a global function that is called when a form should validate the response of a server
     * @param {Function} func
     * @returns {Function}
     */
    FlexCss.Form.registerGlobalRemoteValidationFunction = function (func) {
        FlexCss.Form.globalRemoteValidationFunction = func;
        return FlexCss.Form;
    };

    // initilize forms for all forms
    FlexCss.Form.init("form", {});

}(document, window, jQuery);<commit_msg>restored chat functionallity<commit_after>/**
 * A HTML5 Validation replacement
 */
void function (document, window, $) {
    "use strict";

    var ERROR_CLASS_NAME = 'form-error', INPUT_ERROR_CLASS = 'invalid', LOADING_CLASS = 'loading',
        ARIA_INVALID = 'aria-invalid', REMOTE = 'data-remote', REMOTE_ACTION = 'data-remote-action';
    if (!window.FlexCss) {
        window.FlexCss = {};
    }

    var FlexCss = window.FlexCss;

    /**
     * jQuery helper to serialize a form to json
     * @returns {{}}
     */
    $.fn.serializeFormJSON = function () {

        var o = {};
        var a = this.serializeArray();
        $.each(a, function () {
            if (o[this.name]) {
                if (!o[this.name].push) {
                    o[this.name] = [o[this.name]];
                }
                o[this.name].push(this.value || '');
            } else {
                o[this.name] = this.value || '';
            }
        });
        return o;
    };

    /**
     * Enhanced flexcss forms
     * @param {HTMLElement} form
     * @param {Object} options
     * @constructor
     */
    FlexCss.Form = function (form, options) {

        var self = this;

        self.tooltips = null;

        self.options = {
            createTooltips: true,
            appendError: false,
            ajaxSubmitType: 'POST',
            ajaxJsonContentType: 'application/json; charset=utf-8'
        };

        self.options = $.extend(self.options, options);

        // set form class as widget
        // Forms are very different to classical widgets,
        // we will not use our base widget class for this but just self
        self.hfWidgetInstance = self;

        /**
         * Submits this form, either via ajax or just classical (default)
         * @param {HTMLElement} thisForm
         * @param {Event} e
         * @returns {*}
         */
        self.submitFunction = function (thisForm, e) {
            var shouldUseAjax = thisForm.getAttribute(REMOTE), ajaxPostUrl =
                    thisForm.getAttribute(REMOTE_ACTION) ||
                    thisForm.getAttribute('action') || window.location.href,
                useJson = 'json' === shouldUseAjax;
            thisForm.classList.add(LOADING_CLASS);

            $(thisForm).trigger('flexcss.form.submit', [e, self, thisForm]);

            if (null === shouldUseAjax) {
                return thisForm.submit();
            }

            // prevent form from submit normally
            e.preventDefault();

            // support either JSON request payload or normal payload submission
            var serverCall = useJson ? $.ajax({
                contentType: self.options.ajaxJsonContentType,
                type: self.options.ajaxSubmitType,
                url: ajaxPostUrl,
                data: JSON.stringify($(thisForm).serializeFormJSON())
            }) : $.ajax({
                type: self.options.ajaxSubmitType,
                url: ajaxPostUrl,
                data: $(thisForm).serialize()
            });

            $(thisForm).trigger('flexcss.form.afterAjaxSubmit', [e,self, thisForm]);

            serverCall.then(function (r) {
                (self._remoteValidationFunction || FlexCss.Form.globalRemoteValidationFunction).apply(self, [r]);
                return $.Deferred().resolve(r);
            }).always(function (r) {
                $(thisForm).trigger('flexcss.form.ajaxCompleted', [e,self, thisForm, r]);
                // always remove error class
                thisForm.classList.remove(LOADING_CLASS);
            });
        };

        /**
         * This form
         * @returns {HTMLElement}
         */
        self.getForm = function () {
            return form;
        };

        /**
         * A List of Validators
         * @type {Object}
         * @private
         */
        self._validators = FlexCss.Form.globalValidators;

        /**
         * @type {Function}
         * @private
         */
        self._remoteValidationFunction = null;

        /**
         * Registers a function that handles remote validation
         * @param {Function} func
         * @returns {window.FlexCss.Form}
         */
        self.registerRemoteValidation = function (func) {
            self._remoteValidationFunction = func;
            return self;
        };

        /**
         * @param {HTMLElement} field
         * @param {ValidityState} validity
         * @returns {*}
         * @private
         */
        function _setupErrorMessages(field, validity) {
            return FlexCss.Form.globalErrorMessageHandler ?
                FlexCss.Form.globalErrorMessageHandler.apply(self, [field, validity]) : false;
        }

        /**
         * @param {HTMLElement} thisParent
         * @private
         */
        function _removeElementErrors(thisParent) {
            var errors = thisParent.querySelectorAll('.' + ERROR_CLASS_NAME), inputsWithErrorClasses =
                thisParent.querySelectorAll('.' + INPUT_ERROR_CLASS);
            for (var elementErrorIndex = 0; elementErrorIndex < errors.length; elementErrorIndex++) {
                errors[elementErrorIndex].parentNode.removeChild(errors[elementErrorIndex]);
            }
            for (var inputErrorIndex = 0; inputErrorIndex < inputsWithErrorClasses.length; inputErrorIndex++) {
                inputsWithErrorClasses[inputErrorIndex].classList.remove(INPUT_ERROR_CLASS);
                if (self.tooltips) {
                    self.tooltips.removeTooltip(inputsWithErrorClasses[inputErrorIndex]);
                }
            }
        }

        /**
         * Registers a custom validator
         * @param {String} name
         * @param {Function} validator a validation function should always return either a Future(true) or Future(false)
         * even when the field has been invalidated with `setCustomValidity`, because of different browser bugs
         * we can't rely on that
         * @returns {window.FlexCss.Form}
         */
        self.registerValidator = function (name, validator) {
            self._validators[name] = validator;
            return self;
        };

        /**
         * Runs async validation
         * @param {String} validationRef
         * @param {HTMLElement} field
         * @returns {*}
         * @private
         */
        function _runValidation(validationRef, field) {
            if (!self._validators[validationRef]) {
                throw 'Could not found validator: ' + validationRef;
            }
            var cl = field.classList, future = self._validators[validationRef].apply(self, [field, form]);
            cl.add(LOADING_CLASS);
            future.always(function () {
                cl.remove(LOADING_CLASS);
            });
            return future;
        }

        /**
         * Run custom validations for elements, validations are done async do support XHR Requests or other stuff
         *
         * @param {Array|NodeList} fields
         * @returns {$.Deferred} contains either true if validations passed or false if something went wrong
         * @private
         */
        function _customValidationsForElements(fields) {
            var futures = [], fieldsLength = fields.length, checkedFields = [];
            for (var iVal = 0; iVal < fieldsLength; iVal++) {
                var field = fields[iVal], validationRef = field.getAttribute('data-validate'), validity = field.validity;
                if (self._validators[validationRef]) {
                    // use local validation first and then continue with custom validations
                    if (!validity.customError && !validity.valid) {
                        continue;
                    }
                    checkedFields.push(field);
                    futures.push(_runValidation(validationRef, field));
                }
            }
            return $.when.apply(self, futures).then(function () {
                var allFutures = arguments, l = allFutures.length;
                var result = {
                    checkedFields: checkedFields,
                    foundAnyError: false
                };

                for (var fI = 0; fI < l; fI++) {
                    if (!allFutures[fI]) {
                        result.foundAnyError = true;
                    }
                }
                return $.Deferred().resolve(result);
            });
        }

        /**
         * Will handle errors for given fields
         * @param {Array|NodeList} fields
         * @param {Boolean} removeAllErrors
         */
        self.prepareErrors = function (fields, removeAllErrors) {
            if (removeAllErrors) {
                _removeElementErrors(form);
            }
            // We save all validations in an extra property because we need to reset the validity due some
            // implementation errors in other browsers then chrome
            for (var i = 0; i < fields.length; i++) {
                var field = fields[i], parent = field.parentNode, validity = field.validity;
                field.flexFormsSavedValidity = JSON.parse(JSON.stringify(validity));
                if (validity && !validity.valid) {
                    if (!removeAllErrors) {
                        // Remove current errors:
                        _removeElementErrors(parent);
                    }
                    // setup custom error messages:
                    _setupErrorMessages(field, validity);
                    field.classList.add('invalid');
                    field.setAttribute(ARIA_INVALID, 'true');
                    if (self.options.appendError) {
                        parent.insertAdjacentHTML("beforeend", '
' +
                        field.validationMessage +
                        "
");
                    }
                    field.flexFormsSavedValidationMessage = field.validationMessage;
                } else {
                    field.classList.remove('invalid');
                    field.setAttribute(ARIA_INVALID, 'false');

                    _removeElementErrors(parent);
                }
                // FIXME: We have to reset the custom validity here to allow native validations work again
                field.setCustomValidity('');
            }
        };

        function validateCustomFields() {
            return _customValidationsForElements(
                form.querySelectorAll("[data-validate]"));
        }

        /**
         * Creates an array from a node list with invalid items
         * On Firefox also Fieldset's seems to be invalid, remove them
         * @param list
         * @returns {Array}
         * @private
         */
        function _createArrayFromInvalidFieldList(list) {
            var arr = [];
            for (var i = 0; i < list.length; ++i) {
                var n = list[i];
                if (!(n instanceof HTMLFieldSetElement)) {
                    arr.push(n);
                }
            }
            return arr;
        }

        /**
         * Formats the error content for the tooltip
         * @param {String} error
         * @returns {String}
         * @private
         */
        function _formatErrorTooltip(error) {
            return ' ' + error;
        }

        /**
         * Creates a tooltip at given element, will create a new instance if not created
         * @param {HTMLElement} target
         * @param {Boolean} [remove]
         * @private
         */
        function _showAndOrCreateTooltip(target, remove) {
            if (!self.tooltips && self.options.createTooltips) {
                self.tooltips = new FlexCss.Tooltip(form, {
                    containerClass: 'error-tooltip'
                });
            }

            if(!self.options.createTooltips) {
                return;
            }

            setTimeout(function () {
                if (!target.flexFormsSavedValidity) {
                    return;
                }
                if (!target.flexFormsSavedValidity.valid && target.classList.contains(INPUT_ERROR_CLASS)) {
                        self.tooltips.createTooltip(target,
                            _formatErrorTooltip(target.flexFormsSavedValidationMessage), false);
                } else {
                    if (remove) {
                            self.tooltips.removeTooltip(target);
                    }
                }
            }, 0);

        }

        self.showAndOrCreateTooltip = _showAndOrCreateTooltip;

        /**
         * Initializes validation for a given form, registers event handlers
         */
        function initFormValidation() {
            // Suppress the default bubbles
            var invalidFormFired = false, currentValidationFuture;
            form.addEventListener("invalid", function (e) {
                e.preventDefault();
                var invalidFields = form.querySelectorAll(":invalid");
                var arr = _createArrayFromInvalidFieldList(invalidFields);
                // Prevent fire this N times:
                if (arr.indexOf(e.target) > 0) {
                    return;
                }
                // focus the first field:
                if (arr.length > 0) {
                    setTimeout(function () {
                        arr[0].focus();
                        _showAndOrCreateTooltip(arr[0]);
                    }, 0);
                }
                currentValidationFuture = $.Deferred();

                var validation = validateCustomFields();
                self.prepareErrors(arr, true);
                validation.done(function (r) {
                    self.prepareErrors(r.checkedFields, false);
                    currentValidationFuture.resolve(r);
                    invalidFormFired = false;
                });

            }, true);

            // handle focus out for text elements
            // Will show an error if field was invalid the first time
            form.addEventListener("blur", function (e) {
                if (self.tooltips) {
                    self.tooltips.removeTooltip(e.target);
                }
                var target = e.target, hasError = false;

                if (!_checkIsValidBlurFocusElement(target)) {
                    return;
                }

                if (target.classList.contains(INPUT_ERROR_CLASS)) {
                    hasError = true;
                }
                _customValidationsForElements([e.target]).done(function () {
                    self.prepareErrors([e.target], false);
                    if (!hasError) {
                        _showAndOrCreateTooltip(e.target);
                    }
                });

            }, true);

            /**
             * Validates if target is a valid input field to check blur and focus events
             * @param {HTMLElement} target
             * @returns {boolean}
             */
            function _checkIsValidBlurFocusElement(target) {
                var attr = target.getAttribute('type');
                return !((attr === 'checkbox' || attr === 'option' || attr === 'submit' || !(target instanceof HTMLSelectElement || target instanceof HTMLInputElement ||
                target instanceof HTMLTextAreaElement)));
            }

            // handle focus on input elements
            // will show an error if field is invalid
            form.addEventListener("focus", function (e) {
                // do not track errors for checkbox and radios on focus:
                if (!_checkIsValidBlurFocusElement(e.target)) {
                    return;
                }
                _showAndOrCreateTooltip(e.target);
            }, true);

            // Handle change for checkbox, radios and selects
            form.addEventListener("change", function (e) {
                var name = e.target.getAttribute('name');
                if (name) {
                    var inputs = form.querySelectorAll('[name="' + name + '"]');
                    _customValidationsForElements(inputs).done(function () {
                        self.prepareErrors(inputs, false);
                        _showAndOrCreateTooltip(e.target, true);
                    });
                }
            });

            // prevent default if form is invalid
            var submitListener = function (e) {

                if (form.classList.contains(LOADING_CLASS)) {
                    e.preventDefault();
                    return false;
                }

                form.classList.add(LOADING_CLASS);
                form.removeEventListener("submit", submitListener);
                _removeElementErrors(form);
                e.preventDefault();
                // reset:
                if (form.checkValidity()) {
                    form.addEventListener("submit", submitListener);
                    // Custom validations did never pass
                    currentValidationFuture = $.Deferred();
                    var validation = validateCustomFields();
                    validation.done(function (r) {
                        // focus first invalid field:
                        for (var i = 0; i < r.checkedFields.length; i++) {
                            var f = r.checkedFields[i];
                            if (!f.validity.valid) {
                                f.focus();
                                break;
                            }
                        }
                        self.prepareErrors(r.checkedFields, false);
                        currentValidationFuture.resolve(r);
                    });
                    currentValidationFuture.done(function (r) {
                        form.classList.remove(LOADING_CLASS);
                        if (!r.foundAnyError) {
                            // Handle submitting the form to server:
                            self._handleSubmit(e);
                        }
                    });
                } else {
                    form.classList.remove(LOADING_CLASS);
                    form.addEventListener("submit", submitListener);
                }
            };
            form.addEventListener("submit", submitListener);
        }

        initFormValidation();

        /**
         * Handles submitting, optionally allows to stop submitting
         * @param e
         * @private
         */
        self._handleSubmit = function (e) {
            var obj = {
                abort: false
            };
            $(form).trigger('flexcss.form.beforeSubmit', [e, self, form, obj]);
            if (obj.abort === true) {
                return false;
            }
            self.submitFunction.apply(self, [form, e]);
        };

    };

    FlexCss.Form.globalErrorMessageHandler = function () {
    };

    /**
     * Registers a global event Handler
     * @param errorFunc
     */
    FlexCss.Form.registerErrorMessageHandler = function (errorFunc) {
        FlexCss.Form.globalErrorMessageHandler = errorFunc;
    };

    /**
     * Initialize forms for a specific selector
     * @param {String} selector
     * @param {Object} options
     */
    FlexCss.Form.init = function (selector, options) {
        var forms = selector instanceof HTMLElement ? selector.querySelectorAll('form') : document.querySelectorAll(selector);
        for (var i = 0; i < forms.length; i++) {
            new FlexCss.Form(forms[i], options);
        }
    };

    FlexCss.Form.globalValidators = [];
    FlexCss.Form.globalRemoteValidationFunction = function () {
    };

    /**
     * Registers a global validator that is usable on all form instasnces
     * @param {String} name
     * @param {Function} validator
     * @returns {Function}
     */
    FlexCss.Form.registerValidator = function (name, validator) {
        FlexCss.Form.globalValidators[name] = validator;
        return FlexCss.Form;
    };

    /**
     * Registers a global function that is called when a form should validate the response of a server
     * @param {Function} func
     * @returns {Function}
     */
    FlexCss.Form.registerGlobalRemoteValidationFunction = function (func) {
        FlexCss.Form.globalRemoteValidationFunction = func;
        return FlexCss.Form;
    };

    // initilize forms for all forms
    FlexCss.Form.init("form", {});

}(document, window, jQuery);<|endoftext|>


<commit_before>define(["helper/Basic", "Tone/source/UserMedia", "Test", "Tone/source/Source"],
	function (BasicTests, UserMedia, Test, Source) {

	describe("UserMedia", function(){

		//run the common tests
		BasicTests(UserMedia);

		context("Source Tests", function(){

			it ("can connect the output", function(){
				var extIn = new UserMedia();
				extIn.connect(Test);
				extIn.dispose();
			});

			it ("can be constructed with the input number", function(){
				var extIn = new UserMedia();
				extIn.dispose();
			});

			it ("can be constructed with an options object", function(){
				var extIn = new UserMedia({
					"volume" : -10,
					"mute" : false
				});
				expect(extIn.volume.value).to.be.closeTo(-10, 0.1);
				expect(extIn.mute).to.be.false;
				extIn.dispose();
			});

			it ("indicates if the browser has UserMedia support", function(){
				expect(UserMedia.supported).to.be.a.boolean;
			});

		});

		//if it is a manual test (i.e. there is a person to 'allow' the microphone)
		if (UserMedia.supported){

			context("Opening and closing", function(){

				//long timeout to give testers time to allow the microphone
				this.timeout(100000);

				it ("open returns a promise", function(done){
					var extIn = new UserMedia();
					var promise = extIn.open();
					expect(promise).to.be.instanceOf(Promise);
					promise.then(function(){
						extIn.dispose();
						done();
					});
				});

				it ("can open an input", function(done){
					var extIn = new UserMedia();
					extIn.open().then(function(){
						extIn.dispose();
						done();
					});
				});

				it ("is 'started' after media is open and 'stopped' otherwise", function(done){
					var extIn = new UserMedia();
					expect(extIn.state).to.equal("stopped");
					extIn.open().then(function(){
						expect(extIn.state).to.equal("started");
						extIn.dispose();
						done();
					});
				});

				it ("has a label, group and device id when open", function(done){
					var extIn = new UserMedia();
					extIn.open().then(function(){
						expect(extIn.deviceId).to.be.a("string");
						expect(extIn.groupId).to.be.a("string");
						expect(extIn.label).to.be.a("string");
						extIn.dispose();
						done();
					});
				});

				it ("can close an input", function(done){
					var extIn = new UserMedia();
					extIn.open().then(function(){
						extIn.close();
						extIn.dispose();
						done();
					});
				});

				it ("can enumerate devices", function(done){
					var extIn = new UserMedia();
					extIn.enumerateDevices().then(function(devices){
						expect(devices).to.be.instanceOf(Array);
						extIn.dispose();
						done();
					});
				});
			});
		}

	});
});<commit_msg>testing passing in device ids/index<commit_after>define(["helper/Basic", "Tone/source/UserMedia", "Test", "Tone/source/Source"],
	function (BasicTests, UserMedia, Test, Source) {

	describe("UserMedia", function(){

		//run the common tests
		BasicTests(UserMedia);

		context("Source Tests", function(){

			it ("can connect the output", function(){
				var extIn = new UserMedia();
				extIn.connect(Test);
				extIn.dispose();
			});

			it ("can be constructed with the input number", function(){
				var extIn = new UserMedia();
				extIn.dispose();
			});

			it ("can be constructed with an options object", function(){
				var extIn = new UserMedia({
					"volume" : -10,
					"mute" : false
				});
				expect(extIn.volume.value).to.be.closeTo(-10, 0.1);
				expect(extIn.mute).to.be.false;
				extIn.dispose();
			});

			it ("indicates if the browser has UserMedia support", function(){
				expect(UserMedia.supported).to.be.a.boolean;
			});

		});

		//if it is a manual test (i.e. there is a person to 'allow' the microphone)
		if (UserMedia.supported){

			context("Opening and closing", function(){

				//long timeout to give testers time to allow the microphone
				this.timeout(100000);

				it ("open returns a promise", function(){
					var extIn = new UserMedia();
					var promise = extIn.open();
					expect(promise).to.be.instanceOf(Promise);
					return promise.then(function(){
						extIn.dispose();
					});
				});

				it ("can open an input", function(){
					var extIn = new UserMedia();
					return extIn.open().then(function(){
						extIn.dispose();
					});
				});

				it ("can open an input by name", function(){
					var extIn = new UserMedia();
					return extIn.open("default").then(function(){
						expect(extIn.deviceId).to.equal("default");
						extIn.dispose();
					});
				});

				it ("can open an input by index", function(){
					var extIn = new UserMedia();
					return extIn.open(0).then(function(){
						extIn.dispose();
					});
				});

				it ("throws an error if it cant find the device name", function(){
					var extIn = new UserMedia();
					return extIn.open("doesn't exist").catch(function(){
						extIn.dispose();
					})
				});

				it ("is 'started' after media is open and 'stopped' otherwise", function(){
					var extIn = new UserMedia();
					expect(extIn.state).to.equal("stopped");
					return extIn.open().then(function(){
						expect(extIn.state).to.equal("started");
						extIn.dispose();
					});
				});

				it ("has a label, group and device id when open", function(){
					var extIn = new UserMedia();
					return extIn.open().then(function(){
						expect(extIn.deviceId).to.be.a("string");
						expect(extIn.groupId).to.be.a("string");
						expect(extIn.label).to.be.a("string");
						extIn.dispose();
					});
				});

				it ("can close an input", function(){
					var extIn = new UserMedia();
					return extIn.open().then(function(){
						extIn.close();
						extIn.dispose();
					});
				});

				it ("can enumerate devices", function(){
					return UserMedia.enumerateDevices().then(function(devices){
						expect(devices).to.be.instanceOf(Array);
					});
				});
			});
		}

	});
});<|endoftext|>


<commit_before>function StravaEnhancementSuite(options) {
  this.options = options;

  this.default_to_my_results();
  this.estimated_ftp();
  this.hide_invite_friends();
  this.infinite_scroll();
  this.running_tss();
  this.side_by_side_running();
  this.standard_google_map();
  this.variability_index();

  this.edit_profile();
};

StravaEnhancementSuite.prototype.default_to_my_results = function() {
  if (this.options.default_to_my_results === false) {
    return;
  }

  if (typeof Strava.Labs === 'undefined') {
    return;
  }

  // Default to my results
  var view = Strava.Labs.Activities.SegmentLeaderboardView;
  var fn = view.prototype.render;

  view.prototype.render = function () {
    var elem = jQuery(this.el);
    var result = fn.apply(this, Array.prototype.slice.call(arguments));

    if (!elem.hasClass('once-only')) {
      elem.addClass('once-only').find('.clickable[data-filter=my_results]').click();
    }

    return result;
  };
};

StravaEnhancementSuite.prototype.estimated_ftp = function() {
  if (this.options.estimated_ftp === false) {
    return;
  }

  jQuery('#cpcurve-estimatedCP').click();
};

StravaEnhancementSuite.prototype.hide_invite_friends = function() {
  if (this.options.hide_invite_friends === false) {
    return;
  }

  // "You Should Follow" on dashboard
  jQuery('#suggested-follow-module').hide();

  // "Find Your Friends On Strava" on dashboard
  jQuery('#invite-your-friend-module').hide();

  // "Invite friends" in navbar
  jQuery('header nav a.find-and-invite').parent('li').hide();

  // "Share your rides" on profile
  jQuery('a#embed-athlete-widget').parent('section').hide();
};

StravaEnhancementSuite.prototype.infinite_scroll = function() {
  if (this.options.infinite_scroll === false) {
    return;
  }

  if (window.location.pathname.indexOf('/dashboard') !== 0) {
    return;
  }

  var w = jQuery(window);
  var container = jQuery('.feed-container');

  w.scroll(function() {
    var elem = container.find('a.load-feed');

    if (elem.length === 0) {
      // Can't unbind as we may be waiting for another page to load
      return;
    }

    var offset = 100;
    var elem_top = elem.offset().top;
    var window_top = w.scrollTop();
    var window_bottom = w.height() + window_top;

    if ((elem_top >= window_top + offset) && (elem_top < window_bottom)) {
      elem.click().remove();
    }
  });
};

StravaEnhancementSuite.prototype.edit_profile = function() {
  if (window.location.pathname.indexOf(jQuery('header .user-menu a').attr('href')) !== 0) {
    return;
  }

  jQuery('')
    .css('font-size', '20px')
    .css('margin-left', '8px')
    .attr('href', '/settings/profile')
    .text('(edit)')
    .appendTo('.pageContent h1:first');
};

StravaEnhancementSuite.prototype.running_tss = function() {
  if (this.options.running_tss === false) {
    return;
  }

  if (typeof Strava.Labs === 'undefined') {
    return;
  }

  var TSS_PER_HOUR = {
    'Z1':  60,
    'Z2': 100,
    'Z3': 120,
    'Z4': 200,
    'Z5': 300,
    'Z6': 600
  };

  var view = Strava.Labs.Activities.PaceZones;
  var fn = view.prototype.getPaceZones;

  view.prototype.getPaceZones = function () {
    var result = fn.apply(this, Array.prototype.slice.call(arguments));

    var tss = 0;
    jQuery.each(this.paceZones, function (i, item) {
      // Re-parse the time (eg. "23s" / "32:21")
      var parts = item.time.replace('s', '').split(':').reverse();
      var seconds = parts.reduce(function (prev, cur, idx) {
          return prev + (cur * Math.pow(60, idx));
      }, 0);

      // Calculate the contribution to TSS
      tss += TSS_PER_HOUR[item.name] / 3600 * seconds;
    });

    jQuery('#view .inline-stats').append(
      '
' + Math.round(tss) + '
TSS (estimated)

'
    );

    return result;
  };
};

StravaEnhancementSuite.prototype.side_by_side_running = function() {
  if (this.options.side_by_side_running === false) {
    return;
  }

  jQuery('section.comparison .running-tab').click();
};

StravaEnhancementSuite.prototype.standard_google_map = function() {
  if (this.options.standard_google_map === false) {
    return;
  }

  var poll = function() {
    var elem = jQuery('a.map-type-selector[data-map-type-id=standard]').click();

    if (elem.length > 0) {
      elem.parents('.drop-down-menu').click();
    } else {
      setTimeout(poll, 1000);
    }
  };

  if (jQuery('#map-canvas').length > 0) {
    poll();
  }
};

StravaEnhancementSuite.prototype.switch_units = function() {
  var url = jQuery("a:contains(My Profile)[href^='/athletes/']").attr('href');
  var target = window._measurement_preference == "meters" ? "feet" : "meters";
  var athlete_id = parseInt(url.split('/')[2], 10);

  (new Strava.Athletes.Athlete(url, athlete_id)).save(
    "measurement_preference",
    target,
    {"success": function(x) { window.location.reload(); } }
  );
};

StravaEnhancementSuite.prototype.variability_index = function() {
  if (this.options.variability_index === false) {
    return;
  }

  if (typeof pageview === 'undefined') {
    return;
  }

  var elem = jQuery('span[data-glossary-term=definition-weighted-average-power]')
    .parents('li');

  var np = parseInt(elem.find('strong').text(), 10);
  var ap = pageView.activity().get('avgWatts');

  jQuery('
X
Variability Index

')
    .insertAfter(elem)
    .find('strong')
    .text((np / ap).toFixed(2));
};
<commit_msg>Poll as many pages are too dynamic.<commit_after>function StravaEnhancementSuite(options) {
  this.options = options;

  this.default_to_my_results();
  this.estimated_ftp();
  this.hide_invite_friends();
  this.infinite_scroll();
  this.running_tss();
  this.side_by_side_running();
  this.standard_google_map();
  this.variability_index();

  this.edit_profile();
};

StravaEnhancementSuite.prototype.default_to_my_results = function() {
  if (this.options.default_to_my_results === false) {
    return;
  }

  if (typeof Strava.Labs === 'undefined') {
    return;
  }

  // Default to my results
  var view = Strava.Labs.Activities.SegmentLeaderboardView;
  var fn = view.prototype.render;

  view.prototype.render = function () {
    var elem = jQuery(this.el);
    var result = fn.apply(this, Array.prototype.slice.call(arguments));

    if (!elem.hasClass('once-only')) {
      elem.addClass('once-only').find('.clickable[data-filter=my_results]').click();
    }

    return result;
  };
};

StravaEnhancementSuite.prototype.estimated_ftp = function() {
  if (this.options.estimated_ftp === false) {
    return;
  }

  jQuery('#cpcurve-estimatedCP').click();
};

StravaEnhancementSuite.prototype.hide_invite_friends = function() {
  if (this.options.hide_invite_friends === false) {
    return;
  }

  // "You Should Follow" on dashboard
  jQuery('#suggested-follow-module').hide();

  // "Find Your Friends On Strava" on dashboard
  jQuery('#invite-your-friend-module').hide();

  // "Invite friends" in navbar
  jQuery('header nav a.find-and-invite').parent('li').hide();

  // "Share your rides" on profile
  jQuery('a#embed-athlete-widget').parent('section').hide();
};

StravaEnhancementSuite.prototype.infinite_scroll = function() {
  if (this.options.infinite_scroll === false) {
    return;
  }

  if (window.location.pathname.indexOf('/dashboard') !== 0) {
    return;
  }

  var w = jQuery(window);
  var container = jQuery('.feed-container');

  w.scroll(function() {
    var elem = container.find('a.load-feed');

    if (elem.length === 0) {
      // Can't unbind as we may be waiting for another page to load
      return;
    }

    var offset = 100;
    var elem_top = elem.offset().top;
    var window_top = w.scrollTop();
    var window_bottom = w.height() + window_top;

    if ((elem_top >= window_top + offset) && (elem_top < window_bottom)) {
      elem.click().remove();
    }
  });
};

StravaEnhancementSuite.prototype.edit_profile = function() {
  if (window.location.pathname.indexOf(jQuery('header .user-menu a').attr('href')) !== 0) {
    return;
  }

  jQuery('')
    .css('font-size', '20px')
    .css('margin-left', '8px')
    .attr('href', '/settings/profile')
    .text('(edit)')
    .appendTo('.pageContent h1:first');
};

StravaEnhancementSuite.prototype.running_tss = function() {
  if (this.options.running_tss === false) {
    return;
  }

  if (typeof Strava.Labs === 'undefined') {
    return;
  }

  var TSS_PER_HOUR = {
    'Z1':  60,
    'Z2': 100,
    'Z3': 120,
    'Z4': 200,
    'Z5': 300,
    'Z6': 600
  };

  var view = Strava.Labs.Activities.PaceZones;
  var fn = view.prototype.getPaceZones;

  view.prototype.getPaceZones = function () {
    var result = fn.apply(this, Array.prototype.slice.call(arguments));

    var tss = 0;
    jQuery.each(this.paceZones, function (i, item) {
      // Re-parse the time (eg. "23s" / "32:21")
      var parts = item.time.replace('s', '').split(':').reverse();
      var seconds = parts.reduce(function (prev, cur, idx) {
          return prev + (cur * Math.pow(60, idx));
      }, 0);

      // Calculate the contribution to TSS
      tss += TSS_PER_HOUR[item.name] / 3600 * seconds;
    });

    jQuery('#view .inline-stats').append(
      '
' + Math.round(tss) + '
TSS (estimated)

'
    );

    return result;
  };
};

StravaEnhancementSuite.prototype.side_by_side_running = function() {
  if (this.options.side_by_side_running === false) {
    return;
  }

  jQuery('section.comparison .running-tab').click();
};

StravaEnhancementSuite.prototype.standard_google_map = function() {
  if (this.options.standard_google_map === false) {
    return;
  }

  setInterval(function() {
    var elem = jQuery('a.map-type-selector[data-map-type-id=standard]')
      .not('.once-only');

    elem.each(function() {
      jQuery(this)
        .addClass('once-only')
        .click()
        .parents('.drop-down-menu')
        .click()
      ;
    });
  }, 1000);
};

StravaEnhancementSuite.prototype.switch_units = function() {
  var url = jQuery("a:contains(My Profile)[href^='/athletes/']").attr('href');
  var target = window._measurement_preference == "meters" ? "feet" : "meters";
  var athlete_id = parseInt(url.split('/')[2], 10);

  (new Strava.Athletes.Athlete(url, athlete_id)).save(
    "measurement_preference",
    target,
    {"success": function(x) { window.location.reload(); } }
  );
};

StravaEnhancementSuite.prototype.variability_index = function() {
  if (this.options.variability_index === false) {
    return;
  }

  if (typeof pageView === 'undefined') {
    return;
  }

  var elem = jQuery('span[data-glossary-term=definition-weighted-average-power]')
    .parents('li');

  var np = parseInt(elem.find('strong').text(), 10);
  var ap = pageView.activity().get('avgWatts');

  jQuery('
X
Variability Index

')
    .insertAfter(elem)
    .find('strong')
    .text((np / ap).toFixed(2));
};
<|endoftext|>