<commit_before>/**
* jQuery Twitter Bootstrap Theme Switcher v1.0.3
* https://github.com/jguadagno/bootstrapThemeSwitcher
*
* Copyright 2014, Joseph Guadagno
* Released under Apache 2.0 license
* http://apache.org/licenses/LICENSE-2.0.html
*/
;(function ($, window, document, undefined) {

    "use strict";

    var old = $.fn.bootstrapThemeSwitcher;

    // Constructor
    var BootstrapThemeSwitcher = function (element, options) {

        this.$element = $(element);
        this.settings = $.extend({}, $.fn.bootstrapThemeSwitcher.defaults, options);
        this.themesList = [];
        this.getThemes();

        return this;
    };

    // Prototype
    BootstrapThemeSwitcher.prototype = {
        clear: function () {
            console.log('bootstrapThemeSwitcher.clear');
            return this.$element.each(function () {
                this.$element.empty();
            });
        },
        update: function () {
            console.log('bootstrapThemeSwitcher.update');
            this.getThemes();
        },
        switchTheme: function (name, cssFile) {
            console.log('bootstrapThemeSwitcher.switchTheme: name: "' + name + '", cssFile: "' + cssFile + '"');
            var $this = $(this);
            var settings = $.extend({}, $.fn.bootstrapThemeSwitcher.defaults, $this.data('bootstrapThemeSwitcher'));

            var id = settings.cssThemeLink;

            if (cssFile === undefined) {
                cssFile = this.settings.defaultCssFile;
            }
            if (name === undefined) {
                name = cssFile;
            }

            // Remove any existing bootstrap stylesheet that are not the theme ones
            $('head link[href*="bootstrap.min.css"][id!="' + id + '"]').remove();

            // Replace the theme file
            var selector = '#' + id;
            var cssLink = $(selector);
            if (cssLink.length === 0) {
                var cssLinkHtml = "<link rel='stylesheet' id='" + id + "' href='" + cssFile + "' type='text/css' />";
                var firstCssLink = $('head link[rel="stylesheet"]:first');
                if (firstCssLink.length === 0) {
                    $('head').append(cssLinkHtml);
                } else {
                    firstCssLink.before(cssLinkHtml);
                }
                cssLink = $(selector);
            }
            cssLink.attr('href', cssFile);

            // check to see if they want it to be saved
            if (settings.saveToCookie) {
                if ($.cookie === undefined) {
                    console.warn('bootstrapThemeSwitcher: saveToCookie is set to true but jQuery.cookie is not present');
                    return;
                }
                $.cookie(settings.cookieThemeName, name, { expires: settings.cookieExpiration, path: settings.cookiePath });
                $.cookie(settings.cookieThemeCss, cssFile, { expires: settings.cookieExpiration, path: settings.cookiePath });
            }
        },
        loadThemeFromCookie: function (options) {
            if ($.cookie === undefined) {
                console.warn('bootstrapThemeSwitcher: loadThemeFromCookie was called but jQuery.cookie is not present');
                return;
            }
            var settings = $.extend({}, $.fn.bootstrapThemeSwitcher.defaults, options);
            var themeName = $.cookie(settings.cookieThemeName);
            var themeCss = $.cookie(settings.cookieThemeCss);
            this.switchTheme(themeName, themeCss);

        },
        addTheme: function(name, cssFile, start, deleteCount) {
            if (start === undefined) {
                start == 0;
            }
            if (deleteCount === undefined) {
                deleteCount == 0;
            }
            this.themesList.splice(start, deleteCount, {name: name, cssCdn: cssFile});
            this.addThemesToControl();
        },
        addThemesToControl: function() {

            if (this.$element === undefined) {
                console.error('bootstrapThemeSelector: addThemesToControl: Element is undefined');
                return;
            }

            if (this.themesList === undefined) {
                console.error('bootstrapThemeSelector: addThemesToControl: Themes is undefined');
                return;
            }

            var base = this;

            if (this.$element.is('ul')) {

                console.log('bootstrapThemeSelector: UL element selected');
                this.$element.empty();
                $.each(this.themesList, function (i, value) {

                    var li = $("
")
                        .append("" + value.name + "")
                        .on('click', function () {
                            base.switchTheme(value.name, value.cssCdn);
                        });
                    base.$element.append(li);
                });

            } else if (this.$element.is('select')) {
                console.log('bootstrapThemeSelector: SELECT element selected');
                this.$element.empty();

                $.each(this.themesList, function (i, value) {
                    base.$element.append("<option value='" + value.cssCdn + "'>" + value.name + "</option>");
                });
                this.$element.on('change', function () {
                    var optionSelected = $("option:selected", this);
                    base.switchTheme(optionSelected.text(), optionSelected.val());
                });

            } else {
                console.warn('bootstrapThemeSelector only works with ul or select elements');
            }
        },

        getThemes: function() {

            var base = this;

            if (this.settings.localFeed !== null && this.settings.localFeed !== '') {
                // Get the file

                $.ajax({
                    url: this.settings.localFeed,
                    async: false,
                    dataType: 'json',
                    success: function (data) {
                        base.themesList = data.themes;
                        base.addThemesToControl();
                    },
                    error: function (jqXHR, textStatus, errorThrown) {
                        console.error("Failed to retrieve the local feed from '" + base.settings.localFeed + "'")
                    }

                });
                return;
            }

            $.ajax({
                url: this.settings.bootswatchApiUrl + "/" + this.settings.bootswatchApiVersion,
                async: false,
                dataType: 'json',
                success: function (data) {
                    if (data.themes === undefined) {
                        return null;
                    }
                    base.themesList = data.themes;
                    base.themesList.splice(0,0, {name: 'default',cssCdn: base.settings.defaultCssFile});

                    base.addThemesToControl();
                }
            });
        },
        themes : function (newThemeList) {
            if (newThemeList === undefined) {
                return this.themesList;
            }
            else {
                // TODO: Set the associated control.
                this.themesList = newThemeList;
            }
        }
    };

    // Plugin
    $.fn.bootstrapThemeSwitcher = function (option) {
        var args = Array.prototype.slice.call(arguments, 1);
        var methodReturn;

        var $this = $(this);
        var data = $this.data('bootstrapThemeSwitcher');
        var options = typeof option === 'object' && option;

        if (!data) {
            $this.data('bootstrapThemeSwitcher', (data = new BootstrapThemeSwitcher(this, options) ));
        }
        if (typeof option === 'string') {
            methodReturn = data[ option ].apply(data, args);
        }
        return ( methodReturn === undefined ) ? $this : methodReturn;
    };

    $.fn.bootstrapThemeSwitcher.defaults = {
        cssThemeLink: 'bootstrapTheme',
        saveToCookie: true,
        cookieThemeName: 'bootstrapTheme.name',
        cookieThemeCss: 'boostrapTheme.css',
        cookieExpiration: 7,
        cookiePath: '/',
        defaultCssFile: '//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css',
        bootswatchApiUrl: 'http://api.bootswatch.com',
        bootswatchApiVersion: '3',
        loadFromBootswatch: true,
        localFeed: ''
    };

    $.fn.bootstrapThemeSwitcher.Constructor = BootstrapThemeSwitcher;

    $.fn.bootstrapThemeSwitcher.noConflict = function () {
        $.fn.BootstrapThemeSwitcher = old;
        return this;
    };
})(jQuery, window, document);
<commit_msg>Allow bootswatches to be excluded<commit_after>/**
* jQuery Twitter Bootstrap Theme Switcher v1.0.3
* https://github.com/jguadagno/bootstrapThemeSwitcher
*
* Copyright 2014, Joseph Guadagno
* Released under Apache 2.0 license
* http://apache.org/licenses/LICENSE-2.0.html
*/
;(function ($, window, document, undefined) {

    "use strict";

    var old = $.fn.bootstrapThemeSwitcher;

    // Constructor
    var BootstrapThemeSwitcher = function (element, options) {

        this.$element = $(element);
        this.settings = $.extend({}, $.fn.bootstrapThemeSwitcher.defaults, options);
        this.themesList = [];
        this.getThemes();

        return this;
    };

    // Prototype
    BootstrapThemeSwitcher.prototype = {
        clear: function () {
            console.log('bootstrapThemeSwitcher.clear');
            return this.$element.each(function () {
                this.$element.empty();
            });
        },
        update: function () {
            console.log('bootstrapThemeSwitcher.update');
            this.getThemes();
        },
        switchTheme: function (name, cssFile) {
            console.log('bootstrapThemeSwitcher.switchTheme: name: "' + name + '", cssFile: "' + cssFile + '"');
            var $this = $(this);
            var settings = $.extend({}, $.fn.bootstrapThemeSwitcher.defaults, $this.data('bootstrapThemeSwitcher'));

            var id = settings.cssThemeLink;

            if (cssFile === undefined) {
                cssFile = this.settings.defaultCssFile;
            }
            if (name === undefined) {
                name = cssFile;
            }

            // Remove any existing bootstrap stylesheet that are not the theme ones
            $('head link[href*="bootstrap.min.css"][id!="' + id + '"]').remove();

            // Replace the theme file
            var selector = '#' + id;
            var cssLink = $(selector);
            if (cssLink.length === 0) {
                var cssLinkHtml = "<link rel='stylesheet' id='" + id + "' href='" + cssFile + "' type='text/css' />";
                var firstCssLink = $('head link[rel="stylesheet"]:first');
                if (firstCssLink.length === 0) {
                    $('head').append(cssLinkHtml);
                } else {
                    firstCssLink.before(cssLinkHtml);
                }
                cssLink = $(selector);
            }
            cssLink.attr('href', cssFile);

            // check to see if they want it to be saved
            if (settings.saveToCookie) {
                if ($.cookie === undefined) {
                    console.warn('bootstrapThemeSwitcher: saveToCookie is set to true but jQuery.cookie is not present');
                    return;
                }
                $.cookie(settings.cookieThemeName, name, { expires: settings.cookieExpiration, path: settings.cookiePath });
                $.cookie(settings.cookieThemeCss, cssFile, { expires: settings.cookieExpiration, path: settings.cookiePath });
            }
        },
        loadThemeFromCookie: function (options) {
            if ($.cookie === undefined) {
                console.warn('bootstrapThemeSwitcher: loadThemeFromCookie was called but jQuery.cookie is not present');
                return;
            }
            var settings = $.extend({}, $.fn.bootstrapThemeSwitcher.defaults, options);
            var themeName = $.cookie(settings.cookieThemeName);
            var themeCss = $.cookie(settings.cookieThemeCss);
            this.switchTheme(themeName, themeCss);

        },
        addTheme: function(name, cssFile, start, deleteCount) {
            if (start === undefined) {
                start == 0;
            }
            if (deleteCount === undefined) {
                deleteCount == 0;
            }
            this.themesList.splice(start, deleteCount, {name: name, cssCdn: cssFile});
            this.addThemesToControl();
        },
        addThemesToControl: function() {

            if (this.$element === undefined) {
                console.error('bootstrapThemeSelector: addThemesToControl: Element is undefined');
                return;
            }

            if (this.themesList === undefined) {
                console.error('bootstrapThemeSelector: addThemesToControl: Themes is undefined');
                return;
            }

            //If BootSwatch exclusions are set
            if(this.settings.excludeBootswatch){
              //Split the string on ,
              if(this.settings.excludeBootswatch.indexOf(",") !== -1){
                var excludeBootswatchs = this.settings.excludeBootswatch.split(',');
              }else{
                var excludeBootswatchs = [];
                excludeBootswatchs.push(this.settings.excludeBootswatch);
              }

              var tempThemeList = this.themesList;
              $.each(tempThemeList, function (i, value) {
                if( jQuery.inArray( value.name, excludeBootswatchs ) !== -1 ){
                  tempThemeList.splice(i,1);
                }
              });
              this.themesList = tempThemeList;
            }

            var base = this;

            if (this.$element.is('ul')) {

                console.log('bootstrapThemeSelector: UL element selected');
                this.$element.empty();
                $.each(this.themesList, function (i, value) {

                    var li = $("
")
                        .append("" + value.name + "")
                        .on('click', function () {
                            base.switchTheme(value.name, value.cssCdn);
                        });
                    base.$element.append(li);
                });

            } else if (this.$element.is('select')) {
                console.log('bootstrapThemeSelector: SELECT element selected');
                this.$element.empty();

                $.each(this.themesList, function (i, value) {
                    base.$element.append("<option value='" + value.cssCdn + "'>" + value.name + "</option>");
                });
                this.$element.on('change', function () {
                    var optionSelected = $("option:selected", this);
                    base.switchTheme(optionSelected.text(), optionSelected.val());
                });

            } else {
                console.warn('bootstrapThemeSelector only works with ul or select elements');
            }
        },

        getThemes: function() {

            var base = this;

            if (this.settings.localFeed !== null && this.settings.localFeed !== '') {
                // Get the file

                $.ajax({
                    url: this.settings.localFeed,
                    async: false,
                    dataType: 'json',
                    success: function (data) {
                        base.themesList = data.themes;
                        base.addThemesToControl();
                    },
                    error: function (jqXHR, textStatus, errorThrown) {
                        console.error("Failed to retrieve the local feed from '" + base.settings.localFeed + "'")
                    }

                });
                return;
            }

            $.ajax({
                url: this.settings.bootswatchApiUrl + "/" + this.settings.bootswatchApiVersion,
                async: false,
                dataType: 'json',
                success: function (data) {
                    if (data.themes === undefined) {
                        return null;
                    }
                    base.themesList = data.themes;
                    base.themesList.splice(0,0, {name: 'default',cssCdn: base.settings.defaultCssFile});

                    base.addThemesToControl();
                }
            });
        },
        themes : function (newThemeList) {
            if (newThemeList === undefined) {
                return this.themesList;
            }
            else {
                // TODO: Set the associated control.
                this.themesList = newThemeList;
            }
        }
    };

    // Plugin
    $.fn.bootstrapThemeSwitcher = function (option) {
        var args = Array.prototype.slice.call(arguments, 1);
        var methodReturn;

        var $this = $(this);
        var data = $this.data('bootstrapThemeSwitcher');
        var options = typeof option === 'object' && option;

        if (!data) {
            $this.data('bootstrapThemeSwitcher', (data = new BootstrapThemeSwitcher(this, options) ));
        }
        if (typeof option === 'string') {
            methodReturn = data[ option ].apply(data, args);
        }
        return ( methodReturn === undefined ) ? $this : methodReturn;
    };

    $.fn.bootstrapThemeSwitcher.defaults = {
        cssThemeLink: 'bootstrapTheme',
        saveToCookie: true,
        cookieThemeName: 'bootstrapTheme.name',
        cookieThemeCss: 'boostrapTheme.css',
        cookieExpiration: 7,
        cookiePath: '/',
        defaultCssFile: '//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css',
        bootswatchApiUrl: 'http://api.bootswatch.com',
        bootswatchApiVersion: '3',
        loadFromBootswatch: true,
        localFeed: '',
        excludeBootswatch:''  // Comma seperated list of BootSwatch names e.g. Slate,Yeti
    };

    $.fn.bootstrapThemeSwitcher.Constructor = BootstrapThemeSwitcher;

    $.fn.bootstrapThemeSwitcher.noConflict = function () {
        $.fn.BootstrapThemeSwitcher = old;
        return this;
    };
})(jQuery, window, document);
<|endoftext|>


<commit_before>const https = require('https');
const Packet = require('../packet');

const get = url => new Promise(resolve => {
  const headers = {
    accept: 'application/dns-message'
  };
  https.get(url, { headers }, resolve);
});

const readStream = stream => {
  const buffer = [];
  return new Promise((resolve, reject) => {
    stream
      .on('error', reject)
      .on('data', chunk => buffer.push(chunk))
      .on('end', () => resolve(Buffer.concat(buffer)));
  })
};

/**
 * @docs https://tools.ietf.org/html/rfc8484
 * @param {*} param0
 */
const DOHClient = ({ dns } = {}) => {
  return (name, type = 'A', cls = Packet.CLASS.IN) => {
    const packet = new Packet();
    packet.questions.push({
      name,
      class: cls,
      type: Packet.TYPE[type],
    });
    const query = packet.toBase64URL();
    return Promise
      .resolve()
      .then(() => get(`https://${dns}/dns-query?dns=${query}`))
      .then(readStream)
      .then(Packet.parse)
  };
};

module.exports = DOHClient;<commit_msg>Add RD bit to DoH cient, fix #40<commit_after>const https = require('https');
const Packet = require('../packet');

const get = url => new Promise(resolve => {
  const headers = {
    accept: 'application/dns-message'
  };
  https.get(url, { headers }, resolve);
});

const readStream = stream => {
  const buffer = [];
  return new Promise((resolve, reject) => {
    stream
      .on('error', reject)
      .on('data', chunk => buffer.push(chunk))
      .on('end', () => resolve(Buffer.concat(buffer)));
  })
};

/**
 * @docs https://tools.ietf.org/html/rfc8484
 * @param {*} param0
 */
const DOHClient = ({ dns } = {}) => {
  return (name, type = 'A', cls = Packet.CLASS.IN, { clientIp, recursive = true } = {}) => {
    const packet = new Packet();
    // see https://github.com/song940/node-dns/issues/29
    if (recursive) {
      packet.header.rd = 1;
    }
    if (clientIp) {
      query.additionals.push(Packet.Resource.EDNS([
        Packet.Resource.EDNS.ECS(clientIp)
      ]));
    };
    packet.questions.push({
      name,
      class: cls,
      type: Packet.TYPE[type],
    });
    const query = packet.toBase64URL();
    return Promise
      .resolve()
      .then(() => get(`https://${dns}/dns-query?dns=${query}`))
      .then(readStream)
      .then(Packet.parse)
  };
};

module.exports = DOHClient;<|endoftext|>


<commit_before>(function(){
'use strict';

/**
 * The base class for all VR devices.
 */
function VRDevice() {
  this.hardwareUnitId = 'webvr-polyfill hardwareUnitId';
  this.deviceId = 'webvr-polyfill deviceId';
  this.deviceName = 'webvr-polyfill deviceName';
}

/**
 * The base class for all VR position sensor devices.
 */
function PositionSensorVRDevice() {
}
PositionSensorVRDevice.prototype = new VRDevice();

/**
 * File sensor, for playback of recorded sensor data
 */
function FilePositionSensorVRDevice( options ) {
  this.deviceId = 'pos-sensor:file';
  this.deviceName = 'VR Position Device (pos-sensor:file)';

  this.file = options.file;
  this.data = [];
  this.i = 0;

  this._loadFile( this.file );

  this.hasOrientation = false;
  this.hasPosition    = false;
}
FilePositionSensorVRDevice.prototype = new PositionSensorVRDevice();

FilePositionSensorVRDevice.prototype._loadFile = function( file ) {
  var self = this;

  fetch(file)
    .then(function(resp){ return resp.json() })
    .then(function(resp){
      var data = resp.data;

      var len = data.length;
      for (var i = 0; i < len; i++) {
        var state = data[i];
        self.data.push( state );
      }

    });
};

/**
 * Returns {orientation: {x,y,z,w}, position: {x,y,z}}.
 */
FilePositionSensorVRDevice.prototype.getState = function() {

  var state = {
    hasOrientation: this.hasOrientation,
    orientation: this.hasOrientation ? this.getOrientation( this.i ) : null,
    hasPosition: this.hasPosition,
    position: this.hasPosition ? this.getPosition( this.i ) : null
  }

  this.i++;
  if (this.i >= this.data.length)
    this.i = 0;

  return state;

};

FilePositionSensorVRDevice.prototype.getOrientation = function( i ) {
  var d = this.data[ i ].orientation;

  if (!d) return null;

  var orientation = Array.isArray(d) ?
    { x: d[0], y: d[1], z: d[2], w: d[3] } :
    { x: d.x,  y: d.y,  z: d.z,  w: d.w };

  return orientation;
};

FilePositionSensorVRDevice.prototype.getPosition = function( i ) {
  var d = this.data[ i ].position;

  if (!d) return null;

  var position = Array.isArray(d) ?
    { x: d[0], y: d[1], z: d[2] } :
    { x: d.x,  y: d.y,  z: d.z };

  return position;
};

window.FilePositionSensorVRDevice = FilePositionSensorVRDevice;

})();<commit_msg>file sensor<commit_after>(function(){
'use strict';

/**
 * The base class for all VR devices.
 */
function VRDevice() {
  this.hardwareUnitId = 'webvr-polyfill hardwareUnitId';
  this.deviceId = 'webvr-polyfill deviceId';
  this.deviceName = 'webvr-polyfill deviceName';
}

/**
 * The base class for all VR position sensor devices.
 */
function PositionSensorVRDevice() {
}
PositionSensorVRDevice.prototype = new VRDevice();

/**
 * File sensor, for playback of recorded sensor data
 */
function FilePositionSensorVRDevice( options ) {
  this.deviceId = 'pos-sensor:file';
  this.deviceName = 'VR Position Device (pos-sensor:file)';

  this.file = options.file;
  this.data = [];
  this.i = 0;

  this._loadFile( this.file );

  this.hasOrientation = false;
  this.hasPosition    = false;
}
FilePositionSensorVRDevice.prototype = new PositionSensorVRDevice();

FilePositionSensorVRDevice.prototype._loadFile = function( file ) {
  var self = this;

  fetch(file)
    .then(function(resp){ return resp.json() })
    .then(function(resp){
      var data = resp.data;

      var len = data.length;
      for (var i = 0; i < len; i++) {
        var state = data[i];
        self.data.push( state );
      }

      if (len) {
        if (self.data[0].orientation)
          self.hasOrientation = true;
        if (self.data[0].position)
          self.hasPosition = true;
      }

    });
};

/**
 * Returns {orientation: {x,y,z,w}, position: {x,y,z}}.
 */
FilePositionSensorVRDevice.prototype.getState = function() {

  var state = {
    hasOrientation: this.hasOrientation,
    orientation: this.hasOrientation ? this._getOrientation( this.i ) : null,
    hasPosition: this.hasPosition,
    position: this.hasPosition ? this._getPosition( this.i ) : null
  }

  this.i++;
  if (this.i >= this.data.length)
    this.i = 0;

  return state;

};

FilePositionSensorVRDevice.prototype.getImmediateState = function( i ) {
  return this.getState( i );
};

FilePositionSensorVRDevice.prototype.resetSensor = function() {

};

FilePositionSensorVRDevice.prototype._getOrientation = function( i ) {
  var d = this.data[ i ].orientation;

  if (!d) return null;

  var orientation = Array.isArray(d) ?
    { x: d[0], y: d[1], z: d[2], w: d[3] } :
    { x: d.x,  y: d.y,  z: d.z,  w: d.w };

  return orientation;
};

FilePositionSensorVRDevice.prototype._getPosition = function( i ) {
  var d = this.data[ i ].position;

  if (!d) return null;

  var position = Array.isArray(d) ?
    { x: d[0], y: d[1], z: d[2] } :
    { x: d.x,  y: d.y,  z: d.z };

  return position;
};

window.FilePositionSensorVRDevice = FilePositionSensorVRDevice;

})();<|endoftext|>


<commit_before>const $ = require('jquery');
const _ = require('lodash');
const InputCloneService = require('./InputCloneService');
const InputValueService = require('./InputValueService');
const PseudoRadioInputService = require('./PseudoRadioInputService');
const InputReplacementService = require('./InputReplacementService');
const RepeaterPreviewService = require('./RepeaterPreviewService');
const QueryService = require('./QueryService');
const ActiveFunctionService = require('./ActiveFunctionService');
const RepeaterDataService = require('./RepeaterDataService');

// TODO
// 1. create factory
// 2. handle no preview heading
// 3. default selected radios & checkboxes

class Repeater {
    /**
     * Repeater
     */
    constructor (window) {
        this.repeaterEntries = 0;
        this.savedEntries = 0;
        this.window = window;
        this.state = [];
    }

    /**
     * init
     * @param repeater {HTMLElement}
     */
    init (repeater) {
        // TODO add a unique attr to each repeater instance, then use that
        this.repeater = repeater;

        // TODO DI this, maybe not have an init method either :/
        this.queryService = new QueryService(
            this.repeater,
            {
                name: 'data-repeater-name',
                'add-another-group-text': 'data-repeater-add-another-group-text',
                'add-group-form': 'data-repeater-new-group',
                'add-group-button': 'data-repeater-add-group',
                'add-group-controls': 'data-repeater-new-group-controls',
                'edit-group': 'data-repeater-edit-group',
                'delete-group': 'data-repeater-delete-group',
                forName: 'data-repeater-for-name',
                entryId: 'data-repeater-entry-id',
                'edit-id': 'data-repeater-edit-id',
                'save-group-button': 'data-repeater-save-group',
                'cancel-save-group-button': 'data-repeater-cancel-save',
                updateId: 'data-repeater-update-id',
                addNewGroupText: 'data-repeater-add-new-group-text',
                'max-saved-groups': 'data-repeater-max-entries',
                'preview-colspan': 'data-repeater-preview-colspan',
                'preview-element': 'data-repeater-preview-id',
                'preview-id': 'data-repeater-preview-id',
                'preview-root': 'data-repeater-preview-root',
                'preview-ui': 'data-repeater-preview-ui',
                'preview-heading': 'data-repeater-for-name',
                'preview-placeholder': 'data-repeater-preview-placeholder',
                'preview-update-id': 'data-repeater-preview-update-id',
                'saved-entry-id': 'data-repeater-saved-data-id',
                'saved-entries-root': 'data-repeater-saved-entries-root',
                activeInput: 'data-repeater-active-input'
            }
        );

        // TODO DI these
        this.activeFunctionService = new ActiveFunctionService();
        this.inputCloneService = new InputCloneService();
        this.inputValueService = new InputValueService();
        this.inputReplacementService = new InputReplacementService();
        this.repeaterDataService = new RepeaterDataService(
            this.queryService,
            this.inputCloneService,
            this.inputValueService
        );
        this.repeaterPreviewService = new RepeaterPreviewService(
            this.queryService,
            this.activeFunctionService,
            this.inputValueService
        );
        this.pseudoRadioInputService = new PseudoRadioInputService(
            this.repeater,
            this.queryService.getAttr('name')
        );

        // Preview UI HTML that is dynamically added to preview rows
        this.previewUiHTML = `

                Edit

                Delete

        `;

        const maxItemsAttr = this.repeater.getAttribute(this.queryService.getAttr('max-saved-groups'));

        // Store max repeater groups as an integer
        this.maxSavedGroups = maxItemsAttr === 'false' ? Infinity : parseInt(maxItemsAttr, 10);

        // Initiate pseudo radio service to polyfill radio inputs without name attrs
        this.pseudoRadioInputService.init();

        // Remove repeater group input names to prevent their values being submitted
        this.removeGroupInputNames(this.queryService.get('add-group-form'));

        // Attach the "add new group" handler
        this.queryService.get('add-group-button')
            .addEventListener(
                'click',
                this.activeFunctionService.wrap.bind(
                    this.activeFunctionService,
                    this.queryService.get('add-group-button'),
                    this.handleAddGroup.bind(this)
                )
            );

        // Attach the "save new group" handler
        this.queryService.get('save-group-button')
            .addEventListener('click', this.handleSaveGroup.bind(this));

        // Attach the "cancel new group" handler
        this.queryService.get('cancel-save-group-button')
            .addEventListener('click', this.handleCancelGroup.bind(this));
    }

    /**
     * Handle the add group action
     * @param event
     */
    handleAddGroup (event) {
        event.preventDefault();
        $(this.queryService.get('add-group-form')).show();
        $(this.queryService.get('add-group-button')).addClass('disabled');
    }

    /**
     * Handle the save group action
     * @param event
     */
    handleSaveGroup (event) {
        const colspan = parseInt(this.repeater.getAttribute(this.queryService.getAttr('preview-colspan')), 10);
        const previewUi = document.createElement('td');

        event.preventDefault();

        // Create state object from the current form
        this.state[this.repeaterEntries] = this.createState(this.queryService.get('add-group-form'));

        // Create preview HTML
        const preview = this.repeaterPreviewService.create(
            this.state[this.repeaterEntries],
            this.queryService.get('preview-heading', { all: true })
        );

        // Set preview attributes and append to the DOM
        preview.setAttribute('colspan', colspan);
        preview.setAttribute(this.queryService.getAttr('preview-id'), this.repeaterEntries);

        // Attach preview element to the DOM
        this.queryService.get('preview-root').appendChild(preview);

        // Attach preview UI to preview row
        previewUi.innerHTML = this.previewUiHTML;
        preview.appendChild(previewUi);

        // Attach preview "edit group" handler
        preview.querySelector(this.queryService.getQuery('edit-group')).addEventListener(
            'click',
            this.activeFunctionService.wrap.bind(
                this.activeFunctionService,
                preview.querySelector(this.queryService.getQuery('edit-group')),
                this.handleEditGroup.bind(this, this.repeaterEntries)
            )
        );

        // Attach preview "edit group" handler
        preview.querySelector(this.queryService.getQuery('delete-group')).addEventListener(
            'click',
            this.activeFunctionService.wrap.bind(
                this.activeFunctionService,
                preview.querySelector(this.queryService.getQuery('edit-group')),
                this.handleDeleteGroup.bind(this, this.repeaterEntries)
            )
        );

        // Create saved data
        this.repeaterDataService.create(this.queryService.get('add-group-form'), this.repeaterEntries);

        // Create the edit form
        this.createEditEntryGroup(this.queryService.get('add-group-form'));

        // Remove empty placeholder
        this.removePlaceholder();

        // Reset new repeater group form
        this.resetGroupFields();

        // Update internal state
        this.repeaterEntries++;
        this.savedEntries++;

        if (this.savedEntries < this.maxSavedGroups) {
            $(this.queryService.get('add-group-button')).removeClass('disabled');

            // Update add new group text
            this.queryService.get('add-group-button')
                .innerText = this.repeater.getAttribute(this.queryService.getAttr('add-another-group-text'));
        }

        // Hide new repeater group form
        $(this.queryService.get('add-group-form')).hide();
    }

    /**
     * Convert the "create new repeater group" to a state object
     */
    createState (group) {
        const $inputs = $(group).find(this.queryService.getQuery('name'));

        return $inputs.toArray().reduce((state, input) => {
            if (!state[input.getAttribute(this.queryService.getAttr('name'))]) {
                state[input.getAttribute(this.queryService.getAttr('name'))] = [];
            }

            state[input.getAttribute(this.queryService.getAttr('name'))].push({
                value: this.inputValueService.getValue(input),
                selected: this.inputValueService.getSelected(input),
                ref: input
            });

            return state;
        }, {});
    }

    /**
     * Create an inline edit form beneath each preview row
     * @param group {HTMLElement}
     */
    createEditEntryGroup (group) {
        const preview = this.repeater.querySelector(
            `[${this.queryService.getAttr('preview-id')}="${this.repeaterEntries}"]`
        );
        const clone = group.cloneNode(true);
        const clonedControls = clone.querySelector(this.queryService.getQuery('add-group-controls'));
        const inputsWithState = $(group).find('[name]').toArray().map(input => {
            return this.inputCloneService.clone(input);
        });

        // Add repeater ID to the group
        clone.setAttribute(this.queryService.getAttr('edit-id'), this.repeaterEntries);
        // Remove the new group attr
        clone.removeAttribute(this.queryService.getAttr('add-group-form'));
        // Remove group input name attrs
        this.removeGroupInputNames(clone);
        // Add cloned group after preview, this will act as the edit group
        $(preview).after(clone);
        // Append our "deep" cloned inputs
        inputsWithState.forEach(input => {
            const name = input.getAttribute(this.queryService.getAttr('name'));
            // Replace inputs in clone with "deep" cloned inputs
            this.inputReplacementService.replace(
                [].slice.call(clonedControls.querySelectorAll(`[${this.queryService.getAttr('name')}="${name}"]`)),
                input
            );
        });

        // Refresh radio state
        this.pseudoRadioInputService.refresh();

        // Hide edit form
        $(clone).hide();

        // Add events to the save / cancel UI within the group
        clone.querySelector(this.queryService.getQuery('save-group-button'))
            .addEventListener('click', this.handleUpdateGroup.bind(this, clone, this.repeaterEntries));
        clone.querySelector(this.queryService.getQuery('cancel-save-group-button'))
            .addEventListener('click', this.handleCancelGroupUpdate.bind(this, clone, this.repeaterEntries));
    }

    /**
     * Handle edit group action
     * @param repeaterId {number}
     * @param event
     */
    handleEditGroup (repeaterId, event) {
        const edit = this.queryService.get('preview-root')
            .querySelector(`[${this.queryService.getAttr('edit-id')}="${repeaterId}"]`)

        event.preventDefault();
        this.repeaterPreviewService.toggleUi();
        $(this.queryService.get('add-group-button')).addClass('disabled');
        $(edit).show();
    }

    /**
     * Handle delete group action
     * @param repeaterId {number}
     * @param event
     */
    handleDeleteGroup (repeaterId, event) {
        // remove preview
        const preview = this.queryService.get('preview-root')
            .querySelector(`[${this.queryService.getAttr('preview-id')}="${repeaterId}"]`);

        // remove edit form
        const edit = this.queryService.get('preview-root')
            .querySelector(`[${this.queryService.getAttr('edit-id')}="${repeaterId}"]`);

        // remove saved data
        const saved = this.queryService.get('saved-entries-root')
            .querySelector(`[${this.queryService.getAttr('saved-entry-id')}="${repeaterId}"]`);

        event.preventDefault();

        // Remove DOM
        preview.remove();
        edit.remove();
        saved.remove();

        // Update state
        this.savedEntries--;

        // Enable "add group" button if we have not exceeded max saved entries
        if (this.savedEntries < this.maxSavedGroups) {
            $(this.queryService.get('add-group-button')).removeClass('disabled');
        }

        // Update "add group" button text and add placeholder if we have removed all entries
        if (!this.savedEntries) {
            this.queryService.get('add-group-button').innerText =
                this.repeater.getAttribute(this.queryService.getAttr('add-another-group-text'));
            this.addPlaceholder();
        }
    }

    /**
     * Handle the cancel interaction when adding a new group
     * @param event
     */
    handleCancelGroup (event) {
        if (this.savedEntries < this.maxSavedGroups) {
            $(this.queryService.get('add-group-button')).removeClass('disabled');
        }

        event.preventDefault();
        this.resetGroupFields();
        $(this.queryService.get('add-group-form')).hide();
    }

    /**
     * Reset each of the new group fields
     */
    resetGroupFields () {
        const $tempFormWrapper = $(this.queryService.get('add-group-form'))
            .wrap('<form></form>').closest('form');

        // A catch-all brute-force input reset, wrap the elements in a temporary
        // form element and trigger that form to reset
        $tempFormWrapper.trigger('reset');
        $(this.queryService.get('add-group-form')).unwrap($tempFormWrapper);
    }

    /**
     * Cache a reference to, and remove the empty placeholder
     */
    removePlaceholder () {
        // remove placeholder
        const placeholder = this.queryService.get('preview-placeholder');

        // add a manual cached reference
        this.queryService.updateRef('preview-placeholder', placeholder.cloneNode(true));

        // remove the placeholder from the DOM
        placeholder.remove();
    }

    /**
     * Add empty placeholder to data preview
     */
    addPlaceholder () {
        $(this.queryService.get('preview-root'))
            .prepend(this.queryService.get('preview-placeholder'));
    }

    /**
     * Handle an edit / update save
     * @param group {HTMLElement}
     * @param repeaterId {number}
     * @param event
     */
    handleUpdateGroup (group, repeaterId, event) {
        event.preventDefault();

        // Update state
        this.state[repeaterId] = this.createState(group);

        // Update preview elements
        this.repeaterPreviewService.update(
            this.state[repeaterId],
            this.queryService.get('preview-heading'),
            this.queryService.get('preview-root')
        );

        // Update saved data
        this.repeaterDataService.update(this.state[repeaterId], repeaterId);

        // Enable preview UI
        this.repeaterPreviewService.toggleUi();

        // enable "add group" button
        $(this.queryService.get('add-group-button')).removeClass('disabled');

        // Hide edit group form
        $(group).hide();
    }

    /**
     * Handle an update cancel, inputs will be restored to their un-edited state
     * @param group {HTMLElement|Node}
     * @param repeaterId {number}
     * @param event
     */
    handleCancelGroupUpdate (group, repeaterId, event) {
        event.preventDefault();

        // Reset input values to pre-edited state
        $(group).find(this.queryService.getQuery('name')).each((index, element) => {
            this.state[repeaterId][element.getAttribute(this.queryService.getAttr('name'))]
                .forEach(input => {
                    // Update the value for selected inputs
                    this.inputValueService.setValue(element, input.value, { selected: input.selected });
                });
        });

        // Revert radio inputs to pre-edited values
        this.pseudoRadioInputService.setState(this.state[repeaterId])

        // Enable preview UI
        this.repeaterPreviewService.toggleUi();

        // Enable "add group" form
        $(this.queryService.get('add-group-button')).removeClass('disabled');

        // Hide edit group
        $(group).hide();
    }

    /**
     * Remove name attribute and add custom name attribute, this will
     * omit the input's data from requests
     * @param group {HTMLElement|Node}
     */
    removeGroupInputNames (group) {
        const $inputs = $(group).find('[name]');

        $inputs.each((index, input) => {
            const name = input.getAttribute('name');
            // set name attr on cloned group
            input.setAttribute(this.queryService.getAttr('name'), name);
            // remove custom name attr on group
            input.removeAttribute('name');
        });
    }
}

module.exports = Repeater;
<commit_msg>fix createEditGroup cloning<commit_after>const $ = require('jquery');
const _ = require('lodash');
const InputCloneService = require('./InputCloneService');
const InputValueService = require('./InputValueService');
const PseudoRadioInputService = require('./PseudoRadioInputService');
const InputReplacementService = require('./InputReplacementService');
const RepeaterPreviewService = require('./RepeaterPreviewService');
const QueryService = require('./QueryService');
const ActiveFunctionService = require('./ActiveFunctionService');
const RepeaterDataService = require('./RepeaterDataService');

// TODO
// 1. create factory
// 2. handle no preview heading
// 3. default selected radios & checkboxes

class Repeater {
    /**
     * Repeater
     */
    constructor (window) {
        this.repeaterEntries = 0;
        this.savedEntries = 0;
        this.window = window;
        this.state = [];
    }

    /**
     * init
     * @param repeater {HTMLElement}
     */
    init (repeater) {
        // TODO add a unique attr to each repeater instance, then use that
        this.repeater = repeater;

        // TODO DI this, maybe not have an init method either :/
        this.queryService = new QueryService(
            this.repeater,
            {
                name: 'data-repeater-name',
                'add-another-group-text': 'data-repeater-add-another-group-text',
                'add-group-form': 'data-repeater-new-group',
                'add-group-button': 'data-repeater-add-group',
                'add-group-controls': 'data-repeater-new-group-controls',
                'edit-group': 'data-repeater-edit-group',
                'delete-group': 'data-repeater-delete-group',
                forName: 'data-repeater-for-name',
                entryId: 'data-repeater-entry-id',
                'edit-id': 'data-repeater-edit-id',
                'save-group-button': 'data-repeater-save-group',
                'cancel-save-group-button': 'data-repeater-cancel-save',
                updateId: 'data-repeater-update-id',
                addNewGroupText: 'data-repeater-add-new-group-text',
                'max-saved-groups': 'data-repeater-max-entries',
                'preview-colspan': 'data-repeater-preview-colspan',
                'preview-element': 'data-repeater-preview-id',
                'preview-id': 'data-repeater-preview-id',
                'preview-root': 'data-repeater-preview-root',
                'preview-ui': 'data-repeater-preview-ui',
                'preview-heading': 'data-repeater-for-name',
                'preview-placeholder': 'data-repeater-preview-placeholder',
                'preview-update-id': 'data-repeater-preview-update-id',
                'saved-entry-id': 'data-repeater-saved-data-id',
                'saved-entries-root': 'data-repeater-saved-entries-root',
                activeInput: 'data-repeater-active-input'
            }
        );

        // TODO DI these
        this.activeFunctionService = new ActiveFunctionService();
        this.inputCloneService = new InputCloneService();
        this.inputValueService = new InputValueService();
        this.inputReplacementService = new InputReplacementService();
        this.repeaterDataService = new RepeaterDataService(
            this.queryService,
            this.inputCloneService,
            this.inputValueService
        );
        this.repeaterPreviewService = new RepeaterPreviewService(
            this.queryService,
            this.activeFunctionService,
            this.inputValueService
        );
        this.pseudoRadioInputService = new PseudoRadioInputService(
            this.repeater,
            this.queryService.getAttr('name')
        );

        // Preview UI HTML that is dynamically added to preview rows
        this.previewUiHTML = `

                Edit

                Delete

        `;

        const maxItemsAttr = this.repeater.getAttribute(this.queryService.getAttr('max-saved-groups'));

        // Store max repeater groups as an integer
        this.maxSavedGroups = maxItemsAttr === 'false' ? Infinity : parseInt(maxItemsAttr, 10);

        // Initiate pseudo radio service to polyfill radio inputs without name attrs
        this.pseudoRadioInputService.init();

        // Remove repeater group input names to prevent their values being submitted
        this.removeGroupInputNames(this.queryService.get('add-group-form'));

        // Attach the "add new group" handler
        this.queryService.get('add-group-button')
            .addEventListener(
                'click',
                this.activeFunctionService.wrap.bind(
                    this.activeFunctionService,
                    this.queryService.get('add-group-button'),
                    this.handleAddGroup.bind(this)
                )
            );

        // Attach the "save new group" handler
        this.queryService.get('save-group-button')
            .addEventListener('click', this.handleSaveGroup.bind(this));

        // Attach the "cancel new group" handler
        this.queryService.get('cancel-save-group-button')
            .addEventListener('click', this.handleCancelGroup.bind(this));
    }

    /**
     * Handle the add group action
     * @param event
     */
    handleAddGroup (event) {
        event.preventDefault();
        $(this.queryService.get('add-group-form')).show();
        $(this.queryService.get('add-group-button')).addClass('disabled');
    }

    /**
     * Handle the save group action
     * @param event
     */
    handleSaveGroup (event) {
        const colspan = parseInt(this.repeater.getAttribute(this.queryService.getAttr('preview-colspan')), 10);
        const previewUi = document.createElement('td');

        event.preventDefault();

        // Create state object from the current form
        this.state[this.repeaterEntries] = this.createState(this.queryService.get('add-group-form'));

        // Create preview HTML
        const preview = this.repeaterPreviewService.create(
            this.state[this.repeaterEntries],
            this.queryService.get('preview-heading', { all: true })
        );

        // Set preview attributes and append to the DOM
        preview.setAttribute('colspan', colspan);
        preview.setAttribute(this.queryService.getAttr('preview-id'), this.repeaterEntries);

        // Attach preview element to the DOM
        this.queryService.get('preview-root').appendChild(preview);

        // Attach preview UI to preview row
        previewUi.innerHTML = this.previewUiHTML;
        preview.appendChild(previewUi);

        // Attach preview "edit group" handler
        preview.querySelector(this.queryService.getQuery('edit-group')).addEventListener(
            'click',
            this.activeFunctionService.wrap.bind(
                this.activeFunctionService,
                preview.querySelector(this.queryService.getQuery('edit-group')),
                this.handleEditGroup.bind(this, this.repeaterEntries)
            )
        );

        // Attach preview "edit group" handler
        preview.querySelector(this.queryService.getQuery('delete-group')).addEventListener(
            'click',
            this.activeFunctionService.wrap.bind(
                this.activeFunctionService,
                preview.querySelector(this.queryService.getQuery('edit-group')),
                this.handleDeleteGroup.bind(this, this.repeaterEntries)
            )
        );

        // Create saved data
        this.repeaterDataService.create(this.queryService.get('add-group-form'), this.repeaterEntries);

        // Create the edit form
        this.createEditEntryGroup(this.queryService.get('add-group-form'));

        // Remove empty placeholder
        this.removePlaceholder();

        // Reset new repeater group form
        this.resetGroupFields();

        // Update internal state
        this.repeaterEntries++;
        this.savedEntries++;

        if (this.savedEntries < this.maxSavedGroups) {
            $(this.queryService.get('add-group-button')).removeClass('disabled');

            // Update add new group text
            this.queryService.get('add-group-button')
                .innerText = this.repeater.getAttribute(this.queryService.getAttr('add-another-group-text'));
        }

        // Hide new repeater group form
        $(this.queryService.get('add-group-form')).hide();
    }

    /**
     * Convert the "create new repeater group" to a state object
     */
    createState (group) {
        const $inputs = $(group).find(this.queryService.getQuery('name'));

        return $inputs.toArray().reduce((state, input) => {
            if (!state[input.getAttribute(this.queryService.getAttr('name'))]) {
                state[input.getAttribute(this.queryService.getAttr('name'))] = [];
            }

            state[input.getAttribute(this.queryService.getAttr('name'))].push({
                value: this.inputValueService.getValue(input),
                selected: this.inputValueService.getSelected(input),
                ref: input
            });

            return state;
        }, {});
    }

    /**
     * Create an inline edit form beneath each preview row
     * @param group {HTMLElement}
     */
    createEditEntryGroup (group) {
        const preview = this.repeater.querySelector(
            `[${this.queryService.getAttr('preview-id')}="${this.repeaterEntries}"]`
        );
        const clone = group.cloneNode(true);
        const clonedControls = clone.querySelector(this.queryService.getQuery('add-group-controls'));
        const inputsWithState = $(group)
            .find(this.queryService.getQuery('name'))
            .toArray()
            .map(this.inputCloneService.clone.bind(this.inputCloneService));

        // Add repeater ID to the group
        clone.setAttribute(this.queryService.getAttr('edit-id'), this.repeaterEntries);
        // Remove the new group attr
        clone.removeAttribute(this.queryService.getAttr('add-group-form'));
        // Remove group input name attrs
        this.removeGroupInputNames(clone);
        // Add cloned group after preview, this will act as the edit group
        $(preview).after(clone);
        // Append our "deep" cloned inputs
        inputsWithState.forEach(input => {
            const name = input.getAttribute(this.queryService.getAttr('name'));
            // Replace inputs in clone with "deep" cloned inputs
            this.inputReplacementService.replace(
                [].slice.call(clonedControls.querySelectorAll(`[${this.queryService.getAttr('name')}="${name}"]`)),
                input
            );
        });

        // Refresh radio state
        this.pseudoRadioInputService.refresh();

        // Hide edit form
        $(clone).hide();

        // Add events to the save / cancel UI within the group
        clone.querySelector(this.queryService.getQuery('save-group-button'))
            .addEventListener('click', this.handleUpdateGroup.bind(this, clone, this.repeaterEntries));
        clone.querySelector(this.queryService.getQuery('cancel-save-group-button'))
            .addEventListener('click', this.handleCancelGroupUpdate.bind(this, clone, this.repeaterEntries));
    }

    /**
     * Handle edit group action
     * @param repeaterId {number}
     * @param event
     */
    handleEditGroup (repeaterId, event) {
        const edit = this.queryService.get('preview-root')
            .querySelector(`[${this.queryService.getAttr('edit-id')}="${repeaterId}"]`)

        event.preventDefault();
        this.repeaterPreviewService.toggleUi();
        $(this.queryService.get('add-group-button')).addClass('disabled');
        $(edit).show();
    }

    /**
     * Handle delete group action
     * @param repeaterId {number}
     * @param event
     */
    handleDeleteGroup (repeaterId, event) {
        // remove preview
        const preview = this.queryService.get('preview-root')
            .querySelector(`[${this.queryService.getAttr('preview-id')}="${repeaterId}"]`);

        // remove edit form
        const edit = this.queryService.get('preview-root')
            .querySelector(`[${this.queryService.getAttr('edit-id')}="${repeaterId}"]`);

        // remove saved data
        const saved = this.queryService.get('saved-entries-root')
            .querySelector(`[${this.queryService.getAttr('saved-entry-id')}="${repeaterId}"]`);

        event.preventDefault();

        // Remove DOM
        preview.remove();
        edit.remove();
        saved.remove();

        // Update state
        this.savedEntries--;

        // Enable "add group" button if we have not exceeded max saved entries
        if (this.savedEntries < this.maxSavedGroups) {
            $(this.queryService.get('add-group-button')).removeClass('disabled');
        }

        // Update "add group" button text and add placeholder if we have removed all entries
        if (!this.savedEntries) {
            this.queryService.get('add-group-button').innerText =
                this.repeater.getAttribute(this.queryService.getAttr('add-another-group-text'));
            this.addPlaceholder();
        }
    }

    /**
     * Handle the cancel interaction when adding a new group
     * @param event
     */
    handleCancelGroup (event) {
        if (this.savedEntries < this.maxSavedGroups) {
            $(this.queryService.get('add-group-button')).removeClass('disabled');
        }

        event.preventDefault();
        this.resetGroupFields();
        $(this.queryService.get('add-group-form')).hide();
    }

    /**
     * Reset each of the new group fields
     */
    resetGroupFields () {
        const $tempFormWrapper = $(this.queryService.get('add-group-form'))
            .wrap('<form></form>').closest('form');

        // A catch-all brute-force input reset, wrap the elements in a temporary
        // form element and trigger that form to reset
        $tempFormWrapper.trigger('reset');
        $(this.queryService.get('add-group-form')).unwrap($tempFormWrapper);
    }

    /**
     * Cache a reference to, and remove the empty placeholder
     */
    removePlaceholder () {
        // remove placeholder
        const placeholder = this.queryService.get('preview-placeholder');

        // add a manual cached reference
        this.queryService.updateRef('preview-placeholder', placeholder.cloneNode(true));

        // remove the placeholder from the DOM
        placeholder.remove();
    }

    /**
     * Add empty placeholder to data preview
     */
    addPlaceholder () {
        $(this.queryService.get('preview-root'))
            .prepend(this.queryService.get('preview-placeholder'));
    }

    /**
     * Handle an edit / update save
     * @param group {HTMLElement}
     * @param repeaterId {number}
     * @param event
     */
    handleUpdateGroup (group, repeaterId, event) {
        event.preventDefault();

        // Update state
        this.state[repeaterId] = this.createState(group);

        // Update preview elements
        this.repeaterPreviewService.update(
            this.state[repeaterId],
            this.queryService.get('preview-heading'),
            this.queryService.get('preview-root')
        );

        // Update saved data
        this.repeaterDataService.update(this.state[repeaterId], repeaterId);

        // Enable preview UI
        this.repeaterPreviewService.toggleUi();

        // enable "add group" button
        $(this.queryService.get('add-group-button')).removeClass('disabled');

        // Hide edit group form
        $(group).hide();
    }

    /**
     * Handle an update cancel, inputs will be restored to their un-edited state
     * @param group {HTMLElement|Node}
     * @param repeaterId {number}
     * @param event
     */
    handleCancelGroupUpdate (group, repeaterId, event) {
        event.preventDefault();

        // Reset input values to pre-edited state
        $(group).find(this.queryService.getQuery('name')).each((index, element) => {
            this.state[repeaterId][element.getAttribute(this.queryService.getAttr('name'))]
                .forEach(input => {
                    // Update the value for selected inputs
                    this.inputValueService.setValue(element, input.value, { selected: input.selected });
                });
        });

        // Revert radio inputs to pre-edited values
        this.pseudoRadioInputService.setState(this.state[repeaterId])

        // Enable preview UI
        this.repeaterPreviewService.toggleUi();

        // Enable "add group" form
        $(this.queryService.get('add-group-button')).removeClass('disabled');

        // Hide edit group
        $(group).hide();
    }

    /**
     * Remove name attribute and add custom name attribute, this will
     * omit the input's data from requests
     * @param group {HTMLElement|Node}
     */
    removeGroupInputNames (group) {
        const $inputs = $(group).find('[name]');

        $inputs.each((index, input) => {
            const name = input.getAttribute('name');
            // set name attr on cloned group
            input.setAttribute(this.queryService.getAttr('name'), name);
            // remove custom name attr on group
            input.removeAttribute('name');
        });
    }
}

module.exports = Repeater;
<|endoftext|>


<commit_before>var Trade = (function () {
    'use strict';

    var tradeMarkets, tradeSubmarkets, tradeUnderlyings, tradeContractForms;

    var processOfferings = function (data, market, formName, barrierCategory, submarket, underlying) {
        var selectors = data.selectors;
        var offerings = data.offerings;

        var submarketElements = {}, underlyingElements = {}, contractCategories = {};

        submarketElements['all'] = 'All';

        market_label:
        for (var i = 0, offlen = offerings.length; i < offlen; i++) {
            market = market || 'Forex';
            var mkt = offerings[i].market;
            if (mkt == market) {
                submarket_label:
                for(var j = 0, sbmktlen = offerings[i].available.length; j < sbmktlen; j++) {
                    var loop_submarket = offerings[i].available[j].submarket;
                    if (submarket && submarket != loop_submarket) {
                        continue submarket_label;
                    }
                    underlying_label:
                    for (var k = 0, undrlylen = offerings[i].available[j].available.length; k < undrlylen; k++) {
                        var loop_underlying = offerings[i].available[j].available[k];
                        if(underlying && underlying != loop_underlying.symbol) {
                            continue underlying_label;
                        }
                        for (var l = 0, ctcategorylen = offerings[i].available[j].available[k].available.length; l < ctcategorylen; l++) {
                            var contractCategory = offerings[i].available[j].available[k].available[l].contract_category, isBarrierUndefinedRequired = false;

                            if (!formName) {
                                formName = 'callput';
                                barrierCategory = 'euro_atm';
                            } else if (formName == 'risefall') {
                                formName = 'callput';
                                barrierCategory = 'euro_atm';
                            } else if (formName == 'higherlower') {
                                formName = 'callput';
                                barrierCategory = 'euro_non_atm';
                            }

                            for (var m = 0, ctcategoryavalen = offerings[i].available[j].available[k].available[l].available.length; m < ctcategoryavalen; m++) {
                                for (var property in  offerings[i].available[j].available[k].available[l].available[m]) {
                                    if (offerings[i].available[j].available[k].available[l].available[m].hasOwnProperty(property)) {
                                        var prop_value = offerings[i].available[j].available[k].available[l].available[m][property];
                                        if (property == 'barrier_category') {
                                            if (!barrierCategory) {
                                                barrierCategory = prop_value;
                                                isBarrierUndefinedRequired = true;
                                            }
                                            if (contractCategory && !contractCategories.hasOwnProperty(contractCategory)) {
                                                if (contractCategory == 'callput') {
                                                    if( prop_value == 'euro_atm') {
                                                        contractCategories['risefall'] = 'risefall';
                                                    } else {
                                                        contractCategories['higherlower'] = 'higherlower';
                                                    }
                                                } else {
                                                    contractCategories[contractCategory] = contractCategory;
                                                }
                                            }
                                            if(formName == contractCategory && barrierCategory == prop_value) {
                                                submarketElements[loop_submarket.toLowerCase().replace(/ /g, '_')] = loop_submarket;
                                                underlyingElements[loop_underlying.symbol] = loop_underlying.symbol_display;
                                            }
                                            if (isBarrierUndefinedRequired) {
                                                barrierCategory = undefined;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        if(underlying && underlying == loop_underlying.symbol) {
                            break underlying_label;
                        }
                    }
                    if (submarket && submarket == loop_submarket) {
                        break submarket_label;
                    }
                }
                break market_label;
            } else {
                continue;
            }
        }
        tradeMarkets = Object.keys(selectors.market);
        tradeContractForms = contractCategories;
        tradeSubmarkets = submarketElements;
        tradeUnderlyings = underlyingElements;
    };

    var details = function (offerings, market, formName, submarket, underlying) {
        processOfferings(offerings, market, formName, submarket, underlying);
    };

    return {
        details: details,
        markets: function () { return tradeMarkets; },
        submarkets: function () { return tradeSubmarkets; },
        underlyings: function () { return tradeUnderlyings; },
        contractForms: function () { return tradeContractForms; }
    };

})();
<commit_msg>add contract type to Trade object<commit_after>var Trade = (function () {
    'use strict';

    var tradeMarkets, tradeSubmarkets, tradeUnderlyings, tradeContractForms, contractType = {};

    var processOfferings = function (data, market, formName, barrierCategory, submarket, underlying) {
        var selectors = data.selectors;
        var offerings = data.offerings;

        var submarketElements = {}, underlyingElements = {}, contractCategories = {};

        submarketElements['all'] = 'All';

        market_label:
        for (var i = 0, offlen = offerings.length; i < offlen; i++) {
            market = market || 'Forex';
            var mkt = offerings[i].market;
            if (mkt == market) {
                submarket_label:
                for(var j = 0, sbmktlen = offerings[i].available.length; j < sbmktlen; j++) {
                    var loop_submarket = offerings[i].available[j].submarket;
                    if (submarket && submarket != loop_submarket) {
                        continue submarket_label;
                    }
                    underlying_label:
                    for (var k = 0, undrlylen = offerings[i].available[j].available.length; k < undrlylen; k++) {
                        var loop_underlying = offerings[i].available[j].available[k];
                        if(underlying && underlying != loop_underlying.symbol) {
                            continue underlying_label;
                        }
                        for (var l = 0, ctcategorylen = offerings[i].available[j].available[k].available.length; l < ctcategorylen; l++) {
                            var contractCategory = offerings[i].available[j].available[k].available[l].contract_category, isBarrierUndefinedRequired = false;

                            if (!formName) {
                                formName = 'callput';
                                barrierCategory = 'euro_atm';
                            } else if (formName == 'risefall') {
                                formName = 'callput';
                                barrierCategory = 'euro_atm';
                            } else if (formName == 'higherlower') {
                                formName = 'callput';
                                barrierCategory = 'euro_non_atm';
                            }

                            for (var m = 0, ctcategoryavalen = offerings[i].available[j].available[k].available[l].available.length; m < ctcategoryavalen; m++) {

                                var currentProperties = offerings[i].available[j].available[k].available[l].available[m];

                                if (!contractType[contractCategory]) {
                                    contractType[contractCategory] = [];
                                }

                                if (contractType[contractCategory].indexOf(currentProperties['contract_type']) == -1) {
                                    contractType[contractCategory].push(currentProperties['contract_type']);
                                }

                                for (var property in  offerings[i].available[j].available[k].available[l].available[m]) {
                                    if (offerings[i].available[j].available[k].available[l].available[m].hasOwnProperty(property)) {
                                        var prop_value = offerings[i].available[j].available[k].available[l].available[m][property];
                                        if (property == 'barrier_category') {
                                            if (!barrierCategory) {
                                                barrierCategory = prop_value;
                                                isBarrierUndefinedRequired = true;
                                            }
                                            if (contractCategory && !contractCategories.hasOwnProperty(contractCategory)) {
                                                if (contractCategory == 'callput') {
                                                    if( prop_value == 'euro_atm') {
                                                        contractCategories['risefall'] = 'risefall';
                                                    } else {
                                                        contractCategories['higherlower'] = 'higherlower';
                                                    }
                                                } else {
                                                    contractCategories[contractCategory] = contractCategory;
                                                }

                                            }
                                            if(formName == contractCategory && barrierCategory == prop_value) {
                                                submarketElements[loop_submarket.toLowerCase().replace(/ /g, '_')] = loop_submarket;
                                                underlyingElements[loop_underlying.symbol] = loop_underlying.symbol_display;
                                            }
                                            if (isBarrierUndefinedRequired) {
                                                barrierCategory = undefined;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        if(underlying && underlying == loop_underlying.symbol) {
                            break underlying_label;
                        }
                    }
                    if (submarket && submarket == loop_submarket) {
                        break submarket_label;
                    }
                }
                break market_label;
            } else {
                continue;
            }
        }
        tradeMarkets = Object.keys(selectors.market);
        tradeContractForms = contractCategories;
        tradeSubmarkets = submarketElements;
        tradeUnderlyings = underlyingElements;
    };

    var details = function (offerings, market, formName, submarket, underlying) {
        processOfferings(offerings, market, formName, submarket, underlying);
    };

    return {
        details: details,
        markets: function () { return tradeMarkets; },
        submarkets: function () { return tradeSubmarkets; },
        underlyings: function () { return tradeUnderlyings; },
        contractForms: function () { return tradeContractForms; },
        contractType: function () { return contractType; }
    };

})();
<|endoftext|>


<commit_before>// Copyright 2012-2015, University of Colorado Boulder

/**
 * Google analytics collection for HTML5 sims.
 * Code provided by Google and Hewlett, possibly doctored by PhET.
 *
 * NOTE: Please be careful about modifications, as it relies on external scripts.
 *
 * @author Sam Reid
 */

( function() {
  'use strict';

  var ua = navigator.userAgent;
  var hasIESecurityRestrictions = !!( ua.match( /MSIE/ ) || ua.match( /Trident\// ) || ua.match( /Edge\// ) );

  // If we're in some form of IE running offline, don't attempt to make a cross-origin request.
  // See https://github.com/phetsims/joist/issues/164
  if ( window.location.protocol === 'file:' && hasIESecurityRestrictions ) {
    return;
  }

  // Don't track sims that we didn't develop
  if ( phet.chipper.brand !== 'phet' && phet.chipper.brand !== 'phet-io' ) {
    return;
  }

  var loadType;
  // This is the iOS app
  if ( phet.chipper.queryParameters[ 'phet-app' ] ) {
    loadType = 'phet-app';
  }
  // For the Android app, see https://github.com/phetsims/phet-android-app/issues/16
  else if ( phet.chipper.queryParameters[ 'phet-android-app' ] ) {
    loadType = 'phet-android-app';
  }
  else if ( top !== self ) {
    // Checks to see if this sim is embedded - phetsims/chipper#50
    loadType = 'embedded';
  }
  // TODO Add additional conditions for tracking hits from the installer, etc.
  else {
    loadType = 'default';
  }

  function sendMessages() {
    // {boolean} - Whether an error was detected with anything relating to google analytics.
    // See https://github.com/phetsims/yotta/issues/30
    var googleAnalyticsErrored = false;
    window.addEventListener( 'error', function( event ) {
      if ( event &&
           event.target &&
           event.target.src &&
           event.target.src.indexOf &&
           event.target.src.indexOf( 'google-analytics' ) >= 0 ) {
        googleAnalyticsErrored = true;
      }
    }, true );

    // {boolean} - Whether analytics.js successfully loaded, see https://github.com/phetsims/yotta/issues/30
    var googleAnalyticsLoaded = false;
    function onGoogleAnalyticsLoad() {
      googleAnalyticsLoaded = true;
    }

    var pingParams = 'pingver=3&' +
                     'project=' + encodeURIComponent( phet.chipper.project ) + '&' +
                     'brand=' + encodeURIComponent( phet.chipper.brand ) + '&' +
                     'version=' + encodeURIComponent( phet.chipper.version ) + '&' +
                     'locale=' + encodeURIComponent( phet.chipper.locale ) + '&' +
                     'buildTimestamp=' + encodeURIComponent( phet.chipper.buildTimestamp ) + '&' +
                     'domain=' + encodeURIComponent( document.domain ) + '&' +
                     'href=' + encodeURIComponent( window.location.href ) + '&' +
                     'type=html&' +
                     'timestamp=' + encodeURIComponent( Date.now() ) + '&' +
                     'loadType=' + encodeURIComponent( loadType ) + '&' +
                     'ref=' + encodeURIComponent( document.referrer );

    function pingURL( url ) {
      var img = document.createElement( 'img' );
      img.src = url;
    }

    pingURL( 'https://phet.colorado.edu/yotta/immediate.gif?' + pingParams );

    window.addEventListener( 'load', function( event ) {
      pingURL( 'https://phet.colorado.edu/yotta/sanity.gif?' + pingParams + '&' +
               'gaError=' + encodeURIComponent( googleAnalyticsErrored ) + '&' +
               'gaLoaded=' + encodeURIComponent( googleAnalyticsLoaded ) );
    }, false );

    // Google Analytics snippet for loading the API
    (function( i, s, o, g, r, a, m ) {
      i.GoogleAnalyticsObject = r;
      i[ r ] = i[ r ] || function() {
        (i[ r ].q = i[ r ].q || []).push( arguments );
      }, i[ r ].l = 1 * new Date();
      a = s.createElement( o ), m = s.getElementsByTagName( o )[ 0 ];
      a.async = 1;
      a.src = g;
      m.parentNode.insertBefore( a, m );
    })( window, document, 'script', ( 'https:' === document.location.protocol ? 'https:' : 'http:' ) + '//www.google-analytics.com/analytics.js', 'googleAnalytics' );

    // Applies custom dimensions that are common for our main, third-party, and phet-io tracker
    var phetPageviewOptions = {};
    if ( phet.chipper ) {
      assert && assert( !phet.chipper.buildTimestamp ||
                        ( !!phet.chipper.project && !!phet.chipper.version && !!phet.chipper.locale ),
        'Missing Google Analytics variable in' );

      if ( phet.chipper.project ) {
        phetPageviewOptions.dimension1 = phet.chipper.project; // simName custom dimension
      }
      if ( phet.chipper.version ) {
        phetPageviewOptions.dimension2 = phet.chipper.version; // simVersion custom dimension
      }
      if ( phet.chipper.locale ) {
        phetPageviewOptions.dimension3 = phet.chipper.locale; // simLocale custom dimension
      }
      if ( phet.chipper.buildTimestamp ) {
        phetPageviewOptions.dimension4 = phet.chipper.buildTimestamp; // simBuildTimestamp custom dimension
      }
      phetPageviewOptions.dimension5 = loadType;
      phetPageviewOptions.dimension6 = document.referrer;
    }

    var offlineSimLocation = 'offline/html/' + phet.chipper.project + '_' + phet.chipper.locale;

    // Put our function in the queue, to be invoked when the analytics.js has fully loaded.
    // See https://github.com/phetsims/yotta/issues/30
    window.googleAnalytics( onGoogleAnalyticsLoad );

    // Main PhET tracker
    window.googleAnalytics( 'create', {
      trackingId: 'UA-5033201-1',
      cookieDomain: 'none'
    } );
    if ( window.location.protocol === 'file:' ) {
      window.googleAnalytics( 'set', 'checkProtocolTask', null );
      window.googleAnalytics( 'set', 'checkStorageTask', null );
      window.googleAnalytics( 'set', 'location', offlineSimLocation );
    }
    window.googleAnalytics( 'send', 'pageview', phetPageviewOptions );

    // PhET iO tracker (see https://github.com/phetsims/phetcommon/issues/26)
    if ( phet.chipper.brand === 'phet-io' ) {
      window.googleAnalytics( 'create', {
        trackingId: 'UA-37615182-3',
        cookieDomain: 'none',
        name: 'io'
      } );
      if ( window.location.protocol === 'file:' ) {
        window.googleAnalytics( 'io.set', 'checkProtocolTask', null );
        window.googleAnalytics( 'io.set', 'checkStorageTask', null );
        window.googleAnalytics( 'io.set', 'location', offlineSimLocation );
      }
      window.googleAnalytics( 'io.send', 'pageview', phetPageviewOptions );
    }

    // Third-party PhET tracker (excludes non-third-party usage, see https://github.com/phetsims/yotta/issues/12)
    if ( window.location.protocol !== 'file:' &&
         !document.domain.match( /(.*\.colorado\.edu\.?$)|(^localhost$)|(^127\.0\.0\.1$)/ ) ) {
      window.googleAnalytics( 'create', {
        trackingId: 'UA-37615182-2',
        cookieDomain: 'none',
        name: 'thirdParty'
      } );
      window.googleAnalytics( 'thirdParty.send', 'pageview', phetPageviewOptions );
    }

    // Hewlett tracker
    window.googleAnalytics( 'create', {
      trackingId: 'UA-5033010-35',
      cookieDomain: 'phet.colorado.edu',
      name: 'hewlett'
    } );
    window.googleAnalytics( 'hewlett.send', 'pageview' );

    // External tracker
    if ( phet.chipper.queryParameters.ga ) {
      window.googleAnalytics( 'create', {
        trackingId: phet.chipper.queryParameters.ga,
        cookieDomain: 'none', // don't require the tracking from our site
        name: 'external'
      } );
      window.googleAnalytics( 'external.send', 'pageview', phet.chipper.queryParameters.gaPage || undefined );
    }
  }

  if ( loadType === 'phet-app' ) {
    window.addEventListener( 'load', function() {
      setTimeout( sendMessages, 0 );
    }, false );
  }
  else {
    sendMessages();
  }
} )();
<commit_msg>Add Google Analytics IP address anonymization to requests, see https://github.com/phetsims/phet-ios-app/issues/512<commit_after>// Copyright 2012-2015, University of Colorado Boulder

/**
 * Google analytics collection for HTML5 sims.
 * Code provided by Google and Hewlett, possibly doctored by PhET.
 *
 * NOTE: Please be careful about modifications, as it relies on external scripts.
 *
 * @author Sam Reid
 */

( function() {
  'use strict';

  var ua = navigator.userAgent;
  var hasIESecurityRestrictions = !!( ua.match( /MSIE/ ) || ua.match( /Trident\// ) || ua.match( /Edge\// ) );

  // If we're in some form of IE running offline, don't attempt to make a cross-origin request.
  // See https://github.com/phetsims/joist/issues/164
  if ( window.location.protocol === 'file:' && hasIESecurityRestrictions ) {
    return;
  }

  // Don't track sims that we didn't develop
  if ( phet.chipper.brand !== 'phet' && phet.chipper.brand !== 'phet-io' ) {
    return;
  }

  var loadType;
  // This is the iOS app
  if ( phet.chipper.queryParameters[ 'phet-app' ] ) {
    loadType = 'phet-app';
  }
  // For the Android app, see https://github.com/phetsims/phet-android-app/issues/16
  else if ( phet.chipper.queryParameters[ 'phet-android-app' ] ) {
    loadType = 'phet-android-app';
  }
  else if ( top !== self ) {
    // Checks to see if this sim is embedded - phetsims/chipper#50
    loadType = 'embedded';
  }
  // TODO Add additional conditions for tracking hits from the installer, etc.
  else {
    loadType = 'default';
  }

  function sendMessages() {
    // {boolean} - Whether an error was detected with anything relating to google analytics.
    // See https://github.com/phetsims/yotta/issues/30
    var googleAnalyticsErrored = false;
    window.addEventListener( 'error', function( event ) {
      if ( event &&
           event.target &&
           event.target.src &&
           event.target.src.indexOf &&
           event.target.src.indexOf( 'google-analytics' ) >= 0 ) {
        googleAnalyticsErrored = true;
      }
    }, true );

    // {boolean} - Whether analytics.js successfully loaded, see https://github.com/phetsims/yotta/issues/30
    var googleAnalyticsLoaded = false;
    function onGoogleAnalyticsLoad() {
      googleAnalyticsLoaded = true;
    }

    var pingParams = 'pingver=3&' +
                     'project=' + encodeURIComponent( phet.chipper.project ) + '&' +
                     'brand=' + encodeURIComponent( phet.chipper.brand ) + '&' +
                     'version=' + encodeURIComponent( phet.chipper.version ) + '&' +
                     'locale=' + encodeURIComponent( phet.chipper.locale ) + '&' +
                     'buildTimestamp=' + encodeURIComponent( phet.chipper.buildTimestamp ) + '&' +
                     'domain=' + encodeURIComponent( document.domain ) + '&' +
                     'href=' + encodeURIComponent( window.location.href ) + '&' +
                     'type=html&' +
                     'timestamp=' + encodeURIComponent( Date.now() ) + '&' +
                     'loadType=' + encodeURIComponent( loadType ) + '&' +
                     'ref=' + encodeURIComponent( document.referrer );

    function pingURL( url ) {
      var img = document.createElement( 'img' );
      img.src = url;
    }

    pingURL( 'https://phet.colorado.edu/yotta/immediate.gif?' + pingParams );

    window.addEventListener( 'load', function( event ) {
      pingURL( 'https://phet.colorado.edu/yotta/sanity.gif?' + pingParams + '&' +
               'gaError=' + encodeURIComponent( googleAnalyticsErrored ) + '&' +
               'gaLoaded=' + encodeURIComponent( googleAnalyticsLoaded ) );
    }, false );

    // Google Analytics snippet for loading the API
    (function( i, s, o, g, r, a, m ) {
      i.GoogleAnalyticsObject = r;
      i[ r ] = i[ r ] || function() {
        (i[ r ].q = i[ r ].q || []).push( arguments );
      }, i[ r ].l = 1 * new Date();
      a = s.createElement( o ), m = s.getElementsByTagName( o )[ 0 ];
      a.async = 1;
      a.src = g;
      m.parentNode.insertBefore( a, m );
    })( window, document, 'script', ( 'https:' === document.location.protocol ? 'https:' : 'http:' ) + '//www.google-analytics.com/analytics.js', 'googleAnalytics' );

    // Applies custom dimensions that are common for our main, third-party, and phet-io tracker
    var phetPageviewOptions = {};
    if ( phet.chipper ) {
      assert && assert( !phet.chipper.buildTimestamp ||
                        ( !!phet.chipper.project && !!phet.chipper.version && !!phet.chipper.locale ),
        'Missing Google Analytics variable in' );

      if ( phet.chipper.project ) {
        phetPageviewOptions.dimension1 = phet.chipper.project; // simName custom dimension
      }
      if ( phet.chipper.version ) {
        phetPageviewOptions.dimension2 = phet.chipper.version; // simVersion custom dimension
      }
      if ( phet.chipper.locale ) {
        phetPageviewOptions.dimension3 = phet.chipper.locale; // simLocale custom dimension
      }
      if ( phet.chipper.buildTimestamp ) {
        phetPageviewOptions.dimension4 = phet.chipper.buildTimestamp; // simBuildTimestamp custom dimension
      }
      phetPageviewOptions.dimension5 = loadType;
      phetPageviewOptions.dimension6 = document.referrer;
    }

    var offlineSimLocation = 'offline/html/' + phet.chipper.project + '_' + phet.chipper.locale;

    // Put our function in the queue, to be invoked when the analytics.js has fully loaded.
    // See https://github.com/phetsims/yotta/issues/30
    window.googleAnalytics( onGoogleAnalyticsLoad );

    // Main PhET tracker
    window.googleAnalytics( 'create', {
      trackingId: 'UA-5033201-1',
      cookieDomain: 'none'
    } );
    if ( window.location.protocol === 'file:' ) {
      window.googleAnalytics( 'set', 'checkProtocolTask', null );
      window.googleAnalytics( 'set', 'checkStorageTask', null );
      window.googleAnalytics( 'set', 'location', offlineSimLocation );
    }
    window.googleAnalytics( 'set', 'anonymizeIp', true );
    window.googleAnalytics( 'send', 'pageview', phetPageviewOptions );

    // PhET iO tracker (see https://github.com/phetsims/phetcommon/issues/26)
    if ( phet.chipper.brand === 'phet-io' ) {
      window.googleAnalytics( 'create', {
        trackingId: 'UA-37615182-3',
        cookieDomain: 'none',
        name: 'io'
      } );
      if ( window.location.protocol === 'file:' ) {
        window.googleAnalytics( 'io.set', 'checkProtocolTask', null );
        window.googleAnalytics( 'io.set', 'checkStorageTask', null );
        window.googleAnalytics( 'io.set', 'location', offlineSimLocation );
      }
      window.googleAnalytics( 'io.set', 'anonymizeIp', true );
      window.googleAnalytics( 'io.send', 'pageview', phetPageviewOptions );
    }

    // Third-party PhET tracker (excludes non-third-party usage, see https://github.com/phetsims/yotta/issues/12)
    if ( window.location.protocol !== 'file:' &&
         !document.domain.match( /(.*\.colorado\.edu\.?$)|(^localhost$)|(^127\.0\.0\.1$)/ ) ) {
      window.googleAnalytics( 'create', {
        trackingId: 'UA-37615182-2',
        cookieDomain: 'none',
        name: 'thirdParty'
      } );
      window.googleAnalytics( 'thirdParty.set', 'anonymizeIp', true );
      window.googleAnalytics( 'thirdParty.send', 'pageview', phetPageviewOptions );
    }

    // Hewlett tracker
    window.googleAnalytics( 'create', {
      trackingId: 'UA-5033010-35',
      cookieDomain: 'phet.colorado.edu',
      name: 'hewlett'
    } );
    window.googleAnalytics( 'hewlett.set', 'anonymizeIp', true );
    window.googleAnalytics( 'hewlett.send', 'pageview' );

    // External tracker
    if ( phet.chipper.queryParameters.ga ) {
      window.googleAnalytics( 'create', {
        trackingId: phet.chipper.queryParameters.ga,
        cookieDomain: 'none', // don't require the tracking from our site
        name: 'external'
      } );
      window.googleAnalytics( 'external.set', 'anonymizeIp', true );
      window.googleAnalytics( 'external.send', 'pageview', phet.chipper.queryParameters.gaPage || undefined );
    }
  }

  if ( loadType === 'phet-app' ) {
    window.addEventListener( 'load', function() {
      setTimeout( sendMessages, 0 );
    }, false );
  }
  else {
    sendMessages();
  }
} )();
<|endoftext|>