<commit_before>/**
 * MenuTagMultiselectWidget is a {@link OO.ui.TagMultiselectWidget OO.ui.TagMultiselectWidget} intended
 * to use a menu of selectable options.
 *
 *     @example
 *     // Example: A basic MenuTagMultiselectWidget.
 *     var widget = new OO.ui.MenuTagMultiselectWidget( {
 *         inputPosition: 'outline',
 *         options: [
 *            { data: 'option1', label: 'Option 1' },
 *            { data: 'option2', label: 'Option 2' },
 *            { data: 'option3', label: 'Option 3' },
 *         ],
 *         selected: [ 'option1', 'option2' ]
 *     } );
 *     $( 'body' ).append( widget.$element );
 *
 * @class
 * @extends OO.ui.TagMultiselectWidget
 *
 * @constructor
 * @param {Object} [config] Configuration object
 * @cfg {boolean} [clearInputOnChoose=true] Clear the text input value when a menu option is chosen
 * @cfg {Object} [menu] Configuration object for the menu widget
 * @cfg {jQuery} [$overlay] An overlay for the menu.
 *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
 */
OO.ui.MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget( config ) {
	config = config || {};

	// Parent constructor
	OO.ui.MenuTagMultiselectWidget.parent.call( this, config );

	this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
	this.clearInputOnChoose = config.clearInputOnChoose === undefined || !!config.clearInputOnChoose;
	this.menu = this.createMenuWidget( $.extend( {
		widget: this,
		input: this.hasInput ? this.input : null,
		$input: this.hasInput ? this.input.$input : null,
		filterFromInput: !!this.hasInput,
		$autoCloseIgnore: this.hasInput ?
			this.input.$element : $( [] ),
		$floatableContainer: this.hasInput && this.inputPosition === 'outline' ?
			this.input.$element : this.$element,
		$overlay: this.$overlay,
		disabled: this.isDisabled()
	}, config.menu ) );
	this.addOptions( config.options || [] );

	// Events
	this.menu.connect( this, {
		choose: 'onMenuChoose',
		toggle: 'onMenuToggle'
	} );
	if ( this.hasInput ) {
		this.input.connect( this, { change: 'onInputChange' } );
	}
	this.connect( this, { resize: 'onResize' } );

	// Initialization
	this.$overlay
		.append( this.menu.$element );
	this.$element
		.addClass( 'oo-ui-menuTagMultiselectWidget' );
	// TagMultiselectWidget already does this, but it doesn't work right because this.menu is not yet
	// set up while the parent constructor runs, and #getAllowedValues rejects everything.
	if ( config.selected ) {
		this.setValue( config.selected );
	}
};

/* Initialization */

OO.inheritClass( OO.ui.MenuTagMultiselectWidget, OO.ui.TagMultiselectWidget );

/* Methods */

/**
 * Respond to resize event
 */
OO.ui.MenuTagMultiselectWidget.prototype.onResize = function () {
	// Reposition the menu
	this.menu.position();
};

/**
 * @inheritdoc
 */
OO.ui.MenuTagMultiselectWidget.prototype.onInputFocus = function () {
	// Parent method
	OO.ui.MenuTagMultiselectWidget.parent.prototype.onInputFocus.call( this );

	this.menu.toggle( true );
};

/**
 * Respond to input change event
 */
OO.ui.MenuTagMultiselectWidget.prototype.onInputChange = function () {
	this.menu.toggle( true );
	this.initializeMenuSelection();
};

/**
 * Respond to menu choose event
 *
 * @param {OO.ui.OptionWidget} menuItem Chosen menu item
 */
OO.ui.MenuTagMultiselectWidget.prototype.onMenuChoose = function ( menuItem ) {
	// Add tag
	this.addTag( menuItem.getData(), menuItem.getLabel() );
	if ( this.hasInput && this.clearInputOnChoose ) {
		this.input.setValue( '' );
	}
};

/**
 * Respond to menu toggle event. Reset item highlights on hide.
 *
 * @param {boolean} isVisible The menu is visible
 */
OO.ui.MenuTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
	if ( !isVisible ) {
		this.menu.selectItem( null );
		this.menu.highlightItem( null );
	} else {
		this.initializeMenuSelection();
	}
};

/**
 * @inheritdoc
 */
OO.ui.MenuTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
	var menuItem = this.menu.findItemFromData( tagItem.getData() );
	// Override the base behavior from TagMultiselectWidget; the base behavior
	// in TagMultiselectWidget is to remove the tag to edit it in the input,
	// but in our case, we want to utilize the menu selection behavior, and
	// definitely not remove the item.

	// If there is an input that is used for filtering, erase the value so we don't filter
	if ( this.hasInput && this.menu.filterFromInput ) {
		this.input.setValue( '' );
	}

	// Select the menu item
	this.menu.selectItem( menuItem );

	this.focus();
};

/**
 * Highlight the first selectable item in the menu, if configured.
 *
 * @private
 * @chainable
 */
OO.ui.MenuTagMultiselectWidget.prototype.initializeMenuSelection = function () {
	if ( !this.menu.findSelectedItem() ) {
		this.menu.highlightItem( this.menu.findFirstSelectableItem() );
	}
};

/**
 * @inheritdoc
 */
OO.ui.MenuTagMultiselectWidget.prototype.addTagFromInput = function () {
	var inputValue = this.input.getValue(),
		validated = false,
		highlightedItem = this.menu.findHighlightedItem(),
		item = this.menu.findItemFromData( inputValue );

	// Override the parent method so we add from the menu
	// rather than directly from the input

	// Look for a highlighted item first
	if ( highlightedItem ) {
		validated = this.addTag( highlightedItem.getData(), highlightedItem.getLabel() );
	} else if ( item ) {
		// Look for the element that fits the data
		validated = this.addTag( item.getData(), item.getLabel() );
	} else {
		// Otherwise, add the tag - the method will only add if the
		// tag is valid or if invalid tags are allowed
		validated = this.addTag( inputValue );
	}

	if ( validated ) {
		this.clearInput();
		this.focus();
	}
};

/**
 * Return the visible items in the menu. This is mainly used for when
 * the menu is filtering results.
 *
 * @return {OO.ui.MenuOptionWidget[]} Visible results
 */
OO.ui.MenuTagMultiselectWidget.prototype.getMenuVisibleItems = function () {
	return this.menu.getItems().filter( function ( menuItem ) {
		return menuItem.isVisible();
	} );
};

/**
 * Create the menu for this widget. This is in a separate method so that
 * child classes can override this without polluting the constructor with
 * unnecessary extra objects that will be overidden.
 *
 * @param {Object} menuConfig Configuration options
 * @return {OO.ui.MenuSelectWidget} Menu widget
 */
OO.ui.MenuTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
	return new OO.ui.MenuSelectWidget( menuConfig );
};

/**
 * Add options to the menu
 *
 * @param {Object[]} menuOptions Object defining options
 */
OO.ui.MenuTagMultiselectWidget.prototype.addOptions = function ( menuOptions ) {
	var widget = this,
		items = menuOptions.map( function ( obj ) {
			return widget.createMenuOptionWidget( obj.data, obj.label );
		} );

	this.menu.addItems( items );
};

/**
 * Create a menu option widget.
 *
 * @param {string} data Item data
 * @param {string} [label] Item label
 * @return {OO.ui.OptionWidget} Option widget
 */
OO.ui.MenuTagMultiselectWidget.prototype.createMenuOptionWidget = function ( data, label ) {
	return new OO.ui.MenuOptionWidget( {
		data: data,
		label: label || data
	} );
};

/**
 * Get the menu
 *
 * @return {OO.ui.MenuSelectWidget} Menu
 */
OO.ui.MenuTagMultiselectWidget.prototype.getMenu = function () {
	return this.menu;
};

/**
 * Get the allowed values list
 *
 * @return {string[]} Allowed data values
 */
OO.ui.MenuTagMultiselectWidget.prototype.getAllowedValues = function () {
	var menuDatas = [];
	if ( this.menu ) {
		// If the parent constructor is calling us, we're not ready yet, this.menu is not set up.
		menuDatas = this.menu.getItems().map( function ( menuItem ) {
			return menuItem.getData();
		} );
	}
	return this.allowedValues.concat( menuDatas );
};
<commit_msg>MenuTagMultiselectWidget: Check for empty inputValue in addTagFromInput<commit_after>/**
 * MenuTagMultiselectWidget is a {@link OO.ui.TagMultiselectWidget OO.ui.TagMultiselectWidget} intended
 * to use a menu of selectable options.
 *
 *     @example
 *     // Example: A basic MenuTagMultiselectWidget.
 *     var widget = new OO.ui.MenuTagMultiselectWidget( {
 *         inputPosition: 'outline',
 *         options: [
 *            { data: 'option1', label: 'Option 1' },
 *            { data: 'option2', label: 'Option 2' },
 *            { data: 'option3', label: 'Option 3' },
 *         ],
 *         selected: [ 'option1', 'option2' ]
 *     } );
 *     $( 'body' ).append( widget.$element );
 *
 * @class
 * @extends OO.ui.TagMultiselectWidget
 *
 * @constructor
 * @param {Object} [config] Configuration object
 * @cfg {boolean} [clearInputOnChoose=true] Clear the text input value when a menu option is chosen
 * @cfg {Object} [menu] Configuration object for the menu widget
 * @cfg {jQuery} [$overlay] An overlay for the menu.
 *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
 */
OO.ui.MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget( config ) {
	config = config || {};

	// Parent constructor
	OO.ui.MenuTagMultiselectWidget.parent.call( this, config );

	this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
	this.clearInputOnChoose = config.clearInputOnChoose === undefined || !!config.clearInputOnChoose;
	this.menu = this.createMenuWidget( $.extend( {
		widget: this,
		input: this.hasInput ? this.input : null,
		$input: this.hasInput ? this.input.$input : null,
		filterFromInput: !!this.hasInput,
		$autoCloseIgnore: this.hasInput ?
			this.input.$element : $( [] ),
		$floatableContainer: this.hasInput && this.inputPosition === 'outline' ?
			this.input.$element : this.$element,
		$overlay: this.$overlay,
		disabled: this.isDisabled()
	}, config.menu ) );
	this.addOptions( config.options || [] );

	// Events
	this.menu.connect( this, {
		choose: 'onMenuChoose',
		toggle: 'onMenuToggle'
	} );
	if ( this.hasInput ) {
		this.input.connect( this, { change: 'onInputChange' } );
	}
	this.connect( this, { resize: 'onResize' } );

	// Initialization
	this.$overlay
		.append( this.menu.$element );
	this.$element
		.addClass( 'oo-ui-menuTagMultiselectWidget' );
	// TagMultiselectWidget already does this, but it doesn't work right because this.menu is not yet
	// set up while the parent constructor runs, and #getAllowedValues rejects everything.
	if ( config.selected ) {
		this.setValue( config.selected );
	}
};

/* Initialization */

OO.inheritClass( OO.ui.MenuTagMultiselectWidget, OO.ui.TagMultiselectWidget );

/* Methods */

/**
 * Respond to resize event
 */
OO.ui.MenuTagMultiselectWidget.prototype.onResize = function () {
	// Reposition the menu
	this.menu.position();
};

/**
 * @inheritdoc
 */
OO.ui.MenuTagMultiselectWidget.prototype.onInputFocus = function () {
	// Parent method
	OO.ui.MenuTagMultiselectWidget.parent.prototype.onInputFocus.call( this );

	this.menu.toggle( true );
};

/**
 * Respond to input change event
 */
OO.ui.MenuTagMultiselectWidget.prototype.onInputChange = function () {
	this.menu.toggle( true );
	this.initializeMenuSelection();
};

/**
 * Respond to menu choose event
 *
 * @param {OO.ui.OptionWidget} menuItem Chosen menu item
 */
OO.ui.MenuTagMultiselectWidget.prototype.onMenuChoose = function ( menuItem ) {
	// Add tag
	this.addTag( menuItem.getData(), menuItem.getLabel() );
	if ( this.hasInput && this.clearInputOnChoose ) {
		this.input.setValue( '' );
	}
};

/**
 * Respond to menu toggle event. Reset item highlights on hide.
 *
 * @param {boolean} isVisible The menu is visible
 */
OO.ui.MenuTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
	if ( !isVisible ) {
		this.menu.selectItem( null );
		this.menu.highlightItem( null );
	} else {
		this.initializeMenuSelection();
	}
};

/**
 * @inheritdoc
 */
OO.ui.MenuTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
	var menuItem = this.menu.findItemFromData( tagItem.getData() );
	// Override the base behavior from TagMultiselectWidget; the base behavior
	// in TagMultiselectWidget is to remove the tag to edit it in the input,
	// but in our case, we want to utilize the menu selection behavior, and
	// definitely not remove the item.

	// If there is an input that is used for filtering, erase the value so we don't filter
	if ( this.hasInput && this.menu.filterFromInput ) {
		this.input.setValue( '' );
	}

	// Select the menu item
	this.menu.selectItem( menuItem );

	this.focus();
};

/**
 * Highlight the first selectable item in the menu, if configured.
 *
 * @private
 * @chainable
 */
OO.ui.MenuTagMultiselectWidget.prototype.initializeMenuSelection = function () {
	if ( !this.menu.findSelectedItem() ) {
		this.menu.highlightItem( this.menu.findFirstSelectableItem() );
	}
};

/**
 * @inheritdoc
 */
OO.ui.MenuTagMultiselectWidget.prototype.addTagFromInput = function () {
	var inputValue = this.input.getValue(),
		validated = false,
		highlightedItem = this.menu.findHighlightedItem(),
		item = this.menu.findItemFromData( inputValue );

	if ( !inputValue ) {
		return;
	}

	// Override the parent method so we add from the menu
	// rather than directly from the input

	// Look for a highlighted item first
	if ( highlightedItem ) {
		validated = this.addTag( highlightedItem.getData(), highlightedItem.getLabel() );
	} else if ( item ) {
		// Look for the element that fits the data
		validated = this.addTag( item.getData(), item.getLabel() );
	} else {
		// Otherwise, add the tag - the method will only add if the
		// tag is valid or if invalid tags are allowed
		validated = this.addTag( inputValue );
	}

	if ( validated ) {
		this.clearInput();
		this.focus();
	}
};

/**
 * Return the visible items in the menu. This is mainly used for when
 * the menu is filtering results.
 *
 * @return {OO.ui.MenuOptionWidget[]} Visible results
 */
OO.ui.MenuTagMultiselectWidget.prototype.getMenuVisibleItems = function () {
	return this.menu.getItems().filter( function ( menuItem ) {
		return menuItem.isVisible();
	} );
};

/**
 * Create the menu for this widget. This is in a separate method so that
 * child classes can override this without polluting the constructor with
 * unnecessary extra objects that will be overidden.
 *
 * @param {Object} menuConfig Configuration options
 * @return {OO.ui.MenuSelectWidget} Menu widget
 */
OO.ui.MenuTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
	return new OO.ui.MenuSelectWidget( menuConfig );
};

/**
 * Add options to the menu
 *
 * @param {Object[]} menuOptions Object defining options
 */
OO.ui.MenuTagMultiselectWidget.prototype.addOptions = function ( menuOptions ) {
	var widget = this,
		items = menuOptions.map( function ( obj ) {
			return widget.createMenuOptionWidget( obj.data, obj.label );
		} );

	this.menu.addItems( items );
};

/**
 * Create a menu option widget.
 *
 * @param {string} data Item data
 * @param {string} [label] Item label
 * @return {OO.ui.OptionWidget} Option widget
 */
OO.ui.MenuTagMultiselectWidget.prototype.createMenuOptionWidget = function ( data, label ) {
	return new OO.ui.MenuOptionWidget( {
		data: data,
		label: label || data
	} );
};

/**
 * Get the menu
 *
 * @return {OO.ui.MenuSelectWidget} Menu
 */
OO.ui.MenuTagMultiselectWidget.prototype.getMenu = function () {
	return this.menu;
};

/**
 * Get the allowed values list
 *
 * @return {string[]} Allowed data values
 */
OO.ui.MenuTagMultiselectWidget.prototype.getAllowedValues = function () {
	var menuDatas = [];
	if ( this.menu ) {
		// If the parent constructor is calling us, we're not ready yet, this.menu is not set up.
		menuDatas = this.menu.getItems().map( function ( menuItem ) {
			return menuItem.getData();
		} );
	}
	return this.allowedValues.concat( menuDatas );
};
<|endoftext|>


<commit_before>// Parts of this code are copied from https://github.com/RoonLabs/roon-extension-powermate

"use strict";
var debug = require('debug')('roonrest'),
  debug_verbose = require('debug')('roonrest:verbose'),
  not_registered_error = "The RoonRest extension is not enabled. Please enable it in Roon settings and try again.",
  default_zone_name = 'Hifi',
  transport,
  zones = [];

// Express
var express = require('express'),
  app = express(),
  port = 3000;

function getZoneByNameOrID(arg) {
  // If arg is "default" or "current" then find the best default zone
  debug("Looking up zone " + arg);
  if (arg == "default" || arg == "current") {
    if (mysettings.zone) {
      debug("Using roon settings zone " + mysettings.zone.name);
      arg = mysettings.zone.name;
    } else if (default_zone_name) {
      arg = default_zone_name;
      debug("Using hard-coded default zone " + default_zone_name);
    } else {
      debug("No default found");
      return null;
    }
  }
  if (arg in zones) {
    return zones[arg];
  } else {
    for (var z in zones) {
      if (arg == zones[z].display_name) {
        return zones[z];
      }
    }
  }
  debug("Zone " + arg + " not found");
  return null;
}

// Roon
var RoonApi = require('node-roon-api'),
  RoonApiSettings = require('node-roon-api-settings'),
  RoonApiStatus = require('node-roon-api-status'),
  RoonApiTransport = require('node-roon-api-transport');

var roon = new RoonApi({
  log_level: 'none',
  extension_id: 'roonrest',
  display_name: 'Roon Rest Controller',
  display_version: '0.0.1',
  publisher: 'Matthew Eckhaus',
  email: 'matt@eckha.us',
  website: 'https://github.com/matteck/roonrest',

  core_paired: function (core) {
    transport = core.services.RoonApiTransport;
    transport.subscribe_zones((response, msg) => {
      if (response == "Subscribed") {
        debug('Subscribed to new core');
        zones = msg.zones.reduce((p, e) => (p[e.zone_id] = e) && p, {});
      } else if (response == "Changed") {
        if (msg.zones_removed) {
          debug('Removed zone', response, msg);
          msg.zones_removed.forEach(e => delete (zones[e.zone_id]));
        }
        if (msg.zones_added) {
          msg.zones_added.forEach(e => zones[e.zone_id] = e);
          debug('Added zone', response, msg);
        }
        if (msg.zones_changed) {
          msg.zones_changed.forEach(e => zones[e.zone_id] = e);
          debug_verbose('Changed zone', response, msg);
        }
      }
    });
  },
  core_unpaired: function (core) {
    transport = undefined;
    debug("Core disconnected.")
  }
});

var mysettings = roon.load_config("settings") || {
  zone: null,
};

function makelayout(settings) {
  var l = {
    values: settings,
    layout: [],
    has_error: false
  };

  l.layout.push({
    type: "zone",
    title: "Zone",
    setting: "zone",
  });

  return l;
}

var svc_settings = new RoonApiSettings(roon, {
  get_settings: function (cb) {
    cb(makelayout(mysettings));
  },
  save_settings: function (req, isdryrun, settings) {
    let l = makelayout(settings.values);
    req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l });

    if (!isdryrun && !l.has_error) {
      mysettings = l.values;
      svc_settings.update_settings(l);
      roon.save_config("settings", mysettings);
      update_status();
    }
  }
});

var svc_status = new RoonApiStatus(roon);

roon.init_services({
  required_services: [RoonApiTransport],
  provided_services: [svc_settings, svc_status],
});

function update_status() {
  if (mysettings.hasOwnProperty("zone") && mysettings.zone != null && mysettings.zone.hasOwnProperty("name")) {
    svc_status.set_status("Ready. Zone assigned: " + mysettings.zone.name, false);
  } else if (default_zone_name != undefined) {
    svc_status.set_status("Ready. No zone assigned. Using default zone " + default_zone_name);
  } else {
    svc_status.set_status("Ready. No zone assigned.");
  }
}

update_status();
roon.start_discovery();

// Universal control actions
app.post('/api/v1/zone/all/control/:action(pause)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    debug('Doing pause_all');
    transport.pause_all();
    res.send('OK');
  }
})

// Zone-specific control actions
app.post('/api/v1/zone/:zone/control/:action(play|pause|playpause|stop|previous|next|mute)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var action = req.params['action'];
    debug('Doing action ' + action + ' on ' + req.params['zone'])
    var this_zone = getZoneByNameOrID(req.params['zone']);
    if (this_zone == null) {
      res.status('404').send();
    } else {
      transport.control(this_zone, action);
      res.send('OK');
    }
  }
})

// Settings
app.post('/api/v1/zone/:zone/settings/:name(shuffle|auto_radio)/:value(on|off)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    debug('Doing set ' + req.params['name'] + ' to ' + req.params['value']);
    var settings_object = {};
    var setting_name = req.params['name']
    if (req.params['value'] == 'on') {
      settings_object[setting_name] = 1;
    } else {
      settings_object[setting_name] = 0;
    }
    this_zone = getZoneByNameOrID(req.params['zone']);
    if (this_zone == null) {
      res.status('404').send();
    }
    transport.change_settings(this_zone, settings_object);
    res.send('OK');
  }
})

// Volume
app.post('/api/v1/zone/:zone/volume/:how(absolute|relative|relative_step)/:value', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var how = req.params['how'];
    var value = req.params['value']
    var this_zone = getZoneByNameOrID(req.params['zone']);
    var this_output = this_zone['outputs'][0]['output_id']
    debug('volume how is ' + how);
    transport.change_volume(this_output, how, value);
    res.send('OK');
  }
})

// Get zone properties
app.get('/api/v1/zone/:zone/:prop(state|name|display_name)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var this_zone = getZoneByNameOrID(req.params['zone']);
    if (this_zone == null) {
      res.status('404').send();
    }
    var prop = req.params['prop']
    if (prop == 'name' || prop == 'display_name') {
      res.send(this_zone.display_name ? this_zone.display_name : this_zone.name);
    } else {
      res.send(this_zone.state);
    }
  }
})

app.get('/api/v1', function (req, res) {
  res.send('RoonRest extensions v1')
})

app.get('/api/v1/zones', function (req, res) {
  res.send(JSON.stringify(zones, null, 2));
})

app.listen(port, function () {
  debug('RoonRest extension started, listening on port ' + port)
})
<commit_msg>Set default zone<commit_after>// Parts of this code are copied from https://github.com/RoonLabs/roon-extension-powermate

"use strict";
var debug = require('debug')('roonrest'),
  debug_verbose = require('debug')('roonrest:verbose'),
  not_registered_error = "The RoonRest extension is not enabled. Please enable it in Roon settings and try again.",
  default_zone_name = 'default',
  transport,
  zones = [];

// Express
var express = require('express'),
  app = express(),
  port = 3000;

function getZoneByNameOrID(arg) {
  // If arg is "default" or "current" then find the best default zone
  debug("Looking up zone " + arg);
  if (arg == "default" || arg == "current") {
    if (mysettings.zone) {
      debug("Using roon settings zone " + mysettings.zone.name);
      arg = mysettings.zone.name;
    } else if (default_zone_name) {
      arg = default_zone_name;
      debug("Using hard-coded default zone " + default_zone_name);
    } else {
      debug("No default found");
      return null;
    }
  }
  if (arg in zones) {
    return zones[arg];
  } else {
    for (var z in zones) {
      if (arg == zones[z].display_name) {
        return zones[z];
      }
    }
  }
  debug("Zone " + arg + " not found");
  return null;
}

// Roon
var RoonApi = require('node-roon-api'),
  RoonApiSettings = require('node-roon-api-settings'),
  RoonApiStatus = require('node-roon-api-status'),
  RoonApiTransport = require('node-roon-api-transport');

var roon = new RoonApi({
  log_level: 'none',
  extension_id: 'roonrest',
  display_name: 'Roon Rest Controller',
  display_version: '0.0.1',
  publisher: 'Matthew Eckhaus',
  email: 'matt@eckha.us',
  website: 'https://github.com/matteck/roonrest',

  core_paired: function (core) {
    transport = core.services.RoonApiTransport;
    transport.subscribe_zones((response, msg) => {
      if (response == "Subscribed") {
        debug('Subscribed to new core');
        zones = msg.zones.reduce((p, e) => (p[e.zone_id] = e) && p, {});
      } else if (response == "Changed") {
        if (msg.zones_removed) {
          debug('Removed zone', response, msg);
          msg.zones_removed.forEach(e => delete (zones[e.zone_id]));
        }
        if (msg.zones_added) {
          msg.zones_added.forEach(e => zones[e.zone_id] = e);
          debug('Added zone', response, msg);
        }
        if (msg.zones_changed) {
          msg.zones_changed.forEach(e => zones[e.zone_id] = e);
          debug_verbose('Changed zone', response, msg);
        }
      }
    });
  },
  core_unpaired: function (core) {
    transport = undefined;
    debug("Core disconnected.")
  }
});

var mysettings = roon.load_config("settings") || {
  zone: null,
};

function makelayout(settings) {
  var l = {
    values: settings,
    layout: [],
    has_error: false
  };

  l.layout.push({
    type: "zone",
    title: "Zone",
    setting: "zone",
  });

  return l;
}

var svc_settings = new RoonApiSettings(roon, {
  get_settings: function (cb) {
    cb(makelayout(mysettings));
  },
  save_settings: function (req, isdryrun, settings) {
    let l = makelayout(settings.values);
    req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l });

    if (!isdryrun && !l.has_error) {
      mysettings = l.values;
      svc_settings.update_settings(l);
      roon.save_config("settings", mysettings);
      update_status();
    }
  }
});

var svc_status = new RoonApiStatus(roon);

roon.init_services({
  required_services: [RoonApiTransport],
  provided_services: [svc_settings, svc_status],
});

function update_status() {
  if (mysettings.hasOwnProperty("zone") && mysettings.zone != null && mysettings.zone.hasOwnProperty("name")) {
    svc_status.set_status("Ready. Zone assigned: " + mysettings.zone.name, false);
  } else if (default_zone_name != undefined) {
    svc_status.set_status("Ready. No zone assigned. Using default zone " + default_zone_name);
  } else {
    svc_status.set_status("Ready. No zone assigned.");
  }
}

update_status();
roon.start_discovery();

// Universal control actions
app.post('/api/v1/zone/all/control/:action(pause)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    debug('Doing pause_all');
    transport.pause_all();
    res.send('OK');
  }
})

// Zone-specific control actions
app.post('/api/v1/zone/:zone/control/:action(play|pause|playpause|stop|previous|next|mute)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var action = req.params['action'];
    debug('Doing action ' + action + ' on ' + req.params['zone'])
    var this_zone = getZoneByNameOrID(req.params['zone']);
    if (this_zone == null) {
      res.status('404').send();
    } else {
      transport.control(this_zone, action);
      res.send('OK');
    }
  }
})

// Settings
app.post('/api/v1/zone/:zone/settings/:name(shuffle|auto_radio)/:value(on|off)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    debug('Doing set ' + req.params['name'] + ' to ' + req.params['value']);
    var settings_object = {};
    var setting_name = req.params['name']
    if (req.params['value'] == 'on') {
      settings_object[setting_name] = 1;
    } else {
      settings_object[setting_name] = 0;
    }
    this_zone = getZoneByNameOrID(req.params['zone']);
    if (this_zone == null) {
      res.status('404').send();
    }
    transport.change_settings(this_zone, settings_object);
    res.send('OK');
  }
})

// Volume
app.post('/api/v1/zone/:zone/volume/:how(absolute|relative|relative_step)/:value', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var how = req.params['how'];
    var value = req.params['value']
    var this_zone = getZoneByNameOrID(req.params['zone']);
    var this_output = this_zone['outputs'][0]['output_id']
    debug('volume how is ' + how);
    transport.change_volume(this_output, how, value);
    res.send('OK');
  }
})

// Get zone properties
app.get('/api/v1/zone/:zone/:prop(state|name|display_name)', function (req, res) {
  if (transport == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var this_zone = getZoneByNameOrID(req.params['zone']);
    if (this_zone == null) {
      res.status('404').send();
    }
    var prop = req.params['prop']
    if (prop == 'name' || prop == 'display_name') {
      res.send(this_zone.display_name ? this_zone.display_name : this_zone.name);
    } else {
      res.send(this_zone.state);
    }
  }
})

app.get('/api/v1', function (req, res) {
  res.send('RoonRest extensions v1')
})

app.get('/api/v1/zones', function (req, res) {
  res.send(JSON.stringify(zones, null, 2));
})

app.listen(port, function () {
  debug('RoonRest extension started, listening on port ' + port)
})
<|endoftext|>


<commit_before>zeit.find = {};

zeit.find.Tabs = gocept.Class.extend({
    // Knows about all tabs

    construct: function() {
        log("initializing tabs");
        var self = this;
        self.container = $('cp-forms');
        var div = self.container.appendChild(
            DIV({'class': 'context-views'}))
        self.tabs = [];
        self.tabs_element = div.appendChild(UL());
        MochiKit.Signal.connect(
            self.tabs_element, 'onclick', self, self.switch_tab);
    },

    add: function(tab) {
        // Create a tab inside $('cp-forms')
        var self = this;
        tab.tab_element = self.tabs_element.appendChild(
            LI({},
                A({href: tab.id}, tab.title)))
        tab.container = self.container.appendChild(DIV({id: tab.id}));
        MochiKit.DOM.hideElement(tab.container);
        self.tabs.push(tab);
        if (self.tabs.length == 1) {
            self.activate(tab.id);
        }
    },

    switch_tab: function(event) {
        var self = this;
        var target = event.target();
        var id = target.getAttribute('href');
        self.activate(id);
        event.stop();
    },

    activate: function(id) {
        var self = this;
        forEach(self.tabs, function(tab) {
            if (tab.id == id) {
                MochiKit.DOM.showElement(tab.container);
                MochiKit.DOM.addElementClass(tab.tab_element, 'selected');
            } else {
                MochiKit.DOM.hideElement(tab.container);
                MochiKit.DOM.removeElementClass(tab.tab_element, 'selected');
            }
        });
    },
});

zeit.find.Tab = gocept.Class.extend({

    construct: function(id, title) {
        var self = this;
        self.id = id;
        self.title = title;
        self.selected = false;
    },

});

(function() {

    var ident = MochiKit.Signal.connect(window, 'onload', function() {
        MochiKit.Signal.disconnect(ident);
        zeit.find.tabs = new zeit.find.Tabs();
    });
})();

(function() {

var template_cache = {};

var load_template = function(template_url, json, id, init) {
  var d = MochiKit.Async.doSimpleXMLHttpRequest(template_url);
  d.addCallback(function(result) {
    var t = jsontemplate.Template(result.responseText);
    template_cache[template_url] = t;
    expand_template(t, json, id, init);
  });
  d.addErrback(log_error);
  return d;
};

var expand_template = function(template, json, id, init) {
  var s = template.expand(json);
  MochiKit.Signal.signal(id, 'before-template-expand')
  $(id).innerHTML = s;
  log('template expanded successfully');
  if (!isUndefinedOrNull(init)) {
      init();
      log('initialization successful');
  }
  MochiKit.Signal.signal(id, 'after-template-expand')
};

var json_callback = function(id, init, json) {
  if (isUndefinedOrNull(json)) {
    // Allow the case, where init is not passed to this callback.
    json = init;
    init = null;
  }
  var template_url = json['template_url'];
  var template = template_cache[template_url];
  if (!isUndefinedOrNull(template)) {
    expand_template(template, json, id, init);
    return;
  }
  load_template(template_url, json, id, init);
};

var log_error = function(err) {
    /* the error can be either a normal error or wrapped
       by MochiKit in a GenericError in which case the message
       is the real error. We check whether the message is the real
       error first by checking whether its information is undefined.
       If it is undefined, we fall back on the outer error and display
       information about that */
    var real_error = err.message;
    if (isUndefinedOrNull(real_error.message)) {
        real_error = err;
    }
    console.error(real_error.name + ': ' + real_error.message);
};

var find = function(url) {
  var d = MochiKit.Async.loadJSONDoc(url);
  d.addCallback(json_callback, 'search_result');
  d.addErrback(log_error);
};

var init_search_form = function() {
  MochiKit.Signal.connect('search_button', 'onclick', function(e) {
    var search_result_url = application_url + '/search_result?fulltext=' + $('fulltext').value;
    find(search_result_url);
  });
  MochiKit.Signal.connect('extended_search_button', 'onclick', function(e) {
    if ($('extended_search')) {
        $('extended_search_form').innerHTML = '';
    } else {
        var d = MochiKit.Async.loadJSONDoc(application_url + '/extended_search_form');
        d.addCallback(json_callback, 'extended_search_form');
        d.addErrback(log_error);
    }
  });
  MochiKit.Signal.connect('result_filters_button', 'onclick', function(e) {
    if ($('filter_Zeit')) {
        $('result_filters').innerHTML = '';
    } else {
        var d = MochiKit.Async.loadJSONDoc(application_url + '/result_filters');
        d.addCallback(json_callback, 'result_filters');
        d.addErrback(log_error);
    }
  });
};

var init = function() {
    var tab = new zeit.find.Tab('search_form', 'Suche');
    zeit.find.tabs.add(tab);
    var d = MochiKit.Async.loadJSONDoc(application_url + '/search_form');
    d.addCallback(json_callback, 'search_form', init_search_form);
    d.addErrback(log_error);
};

MochiKit.Signal.connect(window, 'onload', init);

})();

(function() {

    var draggables = [];

    var connect_draggables = function() {
        var results = MochiKit.DOM.getElementsByTagAndClassName(
            'div', 'search_entry', $('search_result'));
        forEach(results, function(result) {
            draggables.push(new MochiKit.DragAndDrop.Draggable(result, {
                starteffect: null,
                endeffect: null}));
        });
    }

    var disconnect_draggables = function() {
        while(draggables.length > 0) {
            draggables.pop().destroy();
        }
    }

    var init = function() {
        var ident = MochiKit.Signal.connect(
            'search_form', 'after-template-expand', function() {
            MochiKit.Signal.disconnect(ident);
            MochiKit.Signal.connect(
                'search_result', 'before-template-expand', disconnect_draggables);
            MochiKit.Signal.connect(
                'search_result', 'after-template-expand', connect_draggables);
            });
    }

    var ident = MochiKit.Signal.connect(window, 'onload', function() {
        MochiKit.Signal.disconnect(ident);
        init();
    });
})();

// Register empty tabs

(function() {
    var ident = MochiKit.Signal.connect(window, 'onload', function() {
        MochiKit.Signal.disconnect(ident);
        zeit.find.tabs.add(new zeit.find.Tab('favorites', 'Favoriten'));
    });
})();

(function() {
    var ident = MochiKit.Signal.connect(window, 'onload', function() {
        MochiKit.Signal.disconnect(ident);
        zeit.find.tabs.add(new zeit.find.Tab('for-this-page', 'Für diese Seite'));
    });
})();
<commit_msg>Major refactoring; we now have a class "View" that represents the JSON + template on the client. We can then hook up to it with event handlers, cleaning up our code.<commit_after>zeit.find = {};

zeit.find.Tabs = gocept.Class.extend({
    // Knows about all tabs

    construct: function() {
        log("initializing tabs");
        var self = this;
        self.container = $('cp-forms');
        var div = self.container.appendChild(
            DIV({'class': 'context-views'}))
        self.tabs = [];
        self.tabs_element = div.appendChild(UL());
        MochiKit.Signal.connect(
            self.tabs_element, 'onclick', self, self.switch_tab);
    },

    add: function(tab) {
        // Create a tab inside $('cp-forms')
        var self = this;
        tab.tab_element = self.tabs_element.appendChild(
            LI({},
                A({href: tab.id}, tab.title)))
        tab.container = self.container.appendChild(DIV({id: tab.id}));
        MochiKit.DOM.hideElement(tab.container);
        self.tabs.push(tab);
        if (self.tabs.length == 1) {
            self.activate(tab.id);
        }
    },

    switch_tab: function(event) {
        var self = this;
        var target = event.target();
        var id = target.getAttribute('href');
        self.activate(id);
        event.stop();
    },

    activate: function(id) {
        var self = this;
        forEach(self.tabs, function(tab) {
            if (tab.id == id) {
                MochiKit.DOM.showElement(tab.container);
                MochiKit.DOM.addElementClass(tab.tab_element, 'selected');
            } else {
                MochiKit.DOM.hideElement(tab.container);
                MochiKit.DOM.removeElementClass(tab.tab_element, 'selected');
            }
        });
    },
});

zeit.find.Tab = gocept.Class.extend({

    construct: function(id, title) {
        var self = this;
        self.id = id;
        self.title = title;
        self.selected = false;
    },

});

zeit.find.View = gocept.Class.extend({
    construct: function(json_url, expansion_id, get_query_string) {
        var self = this;
        self.json_url = json_url;
        self.expansion_id = expansion_id
        self.get_query_string = get_query_string;
        self.template = null;
    },

    render: function() {
        var self = this;
        // XXX dependency on application_url...
        var url = application_url + '/' + self.json_url;
        if (!isUndefinedOrNull(self.get_query_string)) {
            url += "?" + self.get_query_string();
        }
        var d = MochiKit.Async.loadJSONDoc(url);
        // XXX have to wrap in function to retain reference to self
        // otherwise this gets messed up
        d.addCallback(function(json) { self.callback_json(json) });
        d.addErrback(zeit.find.log_error);
    },

    callback_json: function(json) {
        var self = this;
        var template_url = json['template_url'];
        var template = self.template;
        if (!isUndefinedOrNull(template)) {
            self.expand_template(json);
            return;
        }
        self.load_template(template_url, json);
    },

    load_template: function(template_url, json) {
        var self = this;
        var d = MochiKit.Async.doSimpleXMLHttpRequest(template_url);
        d.addCallback(function(result) {
            var t = jsontemplate.Template(result.responseText);
            self.template = t;
            self.expand_template(json);
        });
        d.addErrback(zeit.find.log_error);
        return d;
    },

    expand_template: function(json) {
        var self = this;
        var s = self.template.expand(json);
        MochiKit.Signal.signal(self, 'before-load')
        $(self.expansion_id).innerHTML = s;
        log('template expanded successfully');
        MochiKit.Signal.signal(self, 'load')
    },
});

zeit.find.log_error = function(err) {
    /* the error can be either a normal error or wrapped
       by MochiKit in a GenericError in which case the message
       is the real error. We check whether the message is the real
       error first by checking whether its information is undefined.
       If it is undefined, we fall back on the outer error and display
       information about that */
    var real_error = err.message;
    if (isUndefinedOrNull(real_error.message)) {
        real_error = err;
    }
    console.error(real_error.name + ': ' + real_error.message);
};

(function() {

    var init_search_form = function() {
        MochiKit.Signal.connect('search_button', 'onclick', function(e) {
            search_result.render();
        });
        MochiKit.Signal.connect('extended_search_button', 'onclick', function(e) {
            if ($('extended_search')) {
                $('extended_search_form').innerHTML = '';
            } else {
                extended_search_form.render();
            }
        });
        MochiKit.Signal.connect('result_filters_button', 'onclick', function(e) {
            if ($('filter_Zeit')) {
                $('result_filters').innerHTML = '';
            } else {
                result_filters.render();
            }
        });
    };

    var draggables = [];

    var connect_draggables = function() {
        var results = MochiKit.DOM.getElementsByTagAndClassName(
            'div', 'search_entry', $('search_result'));
        forEach(results, function(result) {
            draggables.push(new MochiKit.DragAndDrop.Draggable(result, {
                starteffect: null,
                endeffect: null}));
        });
    }

    var disconnect_draggables = function() {
        while(draggables.length > 0) {
            draggables.pop().destroy();
        }
    }

    var init = function() {
//        zeit.find.tabs = new zeit.find.Tabs();
//        zeit.find.tabs.add(new zeit.find.Tab('search_form', 'Suche'));
//        zeit.find.tabs.add(new zeit.find.Tab('favorites', 'Favoriten'));
//       zeit.find.tabs.add(new zeit.find.Tab('for-this-page', 'Für diese Seite'));
        search_form.render();
    };

    search_form = new zeit.find.View(
        'search_form', 'search_form');
    search_result = new zeit.find.View(
        'search_result', 'search_result',
        function() {
            return 'fulltext=' + $('fulltext').value;
        });
    extended_search_form = new zeit.find.View(
        'extended_search_form', 'extended_search_form');
    result_filters = new zeit.find.View(
        'result_filters', 'result_filters')

    MochiKit.Signal.connect(window, 'onload', init);
    MochiKit.Signal.connect(search_form, 'load', init_search_form);
    MochiKit.Signal.connect(search_result, 'before-load',
                            disconnect_draggables);
    MochiKit.Signal.connect(search_result, 'load',
                            connect_draggables);

})();
<|endoftext|>


<commit_before>/*jslint browser:true, plusplus:true, vars:true */
/*jshint browser:true, jquery:true */
/*jshint -W040:false */ /* to don't alert on "this" in triggerLoad and triggerError */

(function ($, window, document) {
    'use strict';

    // options
    var options = {
            autoInit: true,
            selector: 'img',
            srcAttr: 'data-src',
            classNojs: 'lazy',
            blankImage: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
            edgeX: 0,
            edgeY: 0,
            throttle: 99,
            visibleOnly: true,
            loadEvent: 'pageshow', // check AJAX-loaded content in jQueryMobile
            updateEvent: 'load orientationchange resize scroll', // page-modified events
            forceEvent: '', // force loading of all elements
            oninit: null, // init handler
            onshow: null, // start loading handler
            onload: null, // load success handler
            onerror: null // error handler
        },
        $window = $(window),
        elements = [],
        viewportTop,
        viewportBottom,
        viewportLeft,
        viewportRight,
        topLazy = 0,
    /*
     waitingMode=0 : no setTimeout
     waitingMode=1 : setTimeout, no deferred events
     waitingMode=2 : setTimeout, deferred events
     */
        waitingMode = 0;

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

    /**
     * Process function/object event handler
     * @param {string} event suffix
     * @param {jQuery} $el
     */
    function triggerEvent(event, $el) {
        $el.trigger('lazy' + event);
        var handler = options['on' + event];
        if (handler) {
            if ($.isFunction(handler)) {
                handler.call($el[0]);
            } else {
                $el
                    .addClass(handler.addClass)
                    .removeClass(handler.removeClass);
            }
        }
        // queue next check as images may be resized after loading of actual file
        queueCheckLazyElements();
    }

    /**
     * Add element to lazy-load list
     * Call: addElement(idx, el) or addElement(el)
     * @param idx
     * @param {HTMLElement} [el]
     */
    function addElement(idx, el) {
        var $el = $(el || idx);

        // prevent duplicates
        if ($el.data('lazied')) {
            return;
        }
        $el
            .data('lazied', 1)
            .removeClass(options.classNojs);

        if (options.blankImage && $el[0].tagName === 'IMG' && !$el.attr('src')) {
            $el.attr('src', options.blankImage);
        }

        triggerEvent('init', $el);

        elements.unshift($el); // push it in the first position as we iterate elements in reverse order
    }

    /**
     * Save visible viewport boundary to viewportXXX variables
     */
    function calcViewport() {
        var scrollTop = $window.scrollTop(),
            scrollLeft = window.pageXOffset || 0,
            edgeX = options.edgeX,
            edgeY = options.edgeY;

        viewportTop = scrollTop - edgeY;
        viewportBottom = scrollTop + (window.innerHeight || $window.height()) + edgeY;
        viewportLeft = scrollLeft - edgeX;
        viewportRight = scrollLeft + (window.innerWidth || $window.width()) + edgeX;
    }

    /**
     * Trigger onload handler
     */
    function triggerLoad() {
        triggerEvent('load', $(this));
    }

    /**
     * Trigger onerror handler
     */
    function triggerError() {
        triggerEvent('error', $(this));
    }

    /**
     * Load visible elements
     * @param {bool} [force] loading of all elements
     */
    function checkLazyElements(force) {
        if (!elements.length) {
            return;
        }

        topLazy = Infinity;
        calcViewport();

        var i = elements.length - 1,
            srcAttr = options.srcAttr,
            isFuncSrcAttr = $.isFunction(srcAttr);
        for (; i >= 0; i--) {
            var $el = elements[i],
                el = $el[0];

            // remove items that are not in DOM
            if (!$.contains(document.body, el)) {
                elements.splice(i, 1);
            } else if (force || !options.visibleOnly || el.offsetWidth > 0 || el.offsetHeight > 0) {
                var offset = $el.offset(),
                    elTop = offset.top,
                    elLeft = offset.left;

                if (force ||
                    ((elTop < viewportBottom) && (elTop + $el.height() > viewportTop) &&
                        (elLeft < viewportRight) && (elLeft + $el.width() > viewportLeft))) {

                    triggerEvent('show', $el);

                    var src = isFuncSrcAttr ? srcAttr($el) : $el.attr(srcAttr);
                    if (src) {
                        $el
                            .on('load', triggerLoad)
                            .on('error', triggerError)
                            .attr('src', src);
                    }

                    elements.splice(i, 1);
                } else {
                    if (elTop < topLazy) {
                        topLazy = elTop;
                    }
                }
            }
        }
    }

    /**
     * Run check of lazy elements after timeout
     */
    function timeoutLazyElements() {
        if (waitingMode > 1) {
            waitingMode = 1;
            checkLazyElements();
            setTimeout(timeoutLazyElements, options.throttle);
        } else {
            waitingMode = 0;
        }
    }

    /**
     * Queue check of lazy elements because of event e
     * @param {Event} [e]
     */
    function queueCheckLazyElements(e) {
        if (!elements.length) {
            return;
        }

        // fast check for scroll event without new visible elements
        if (e && e.type === 'scroll') {
            calcViewport();
            if (topLazy >= viewportBottom) {
                return;
            }
        }

        if (!waitingMode) {
            waitingMode = 2;
            timeoutLazyElements();
        } else {
            waitingMode = 2;
        }
    }

    /**
     * Add batch of new elements: $(container).lazyLoadXT([optional selector])
     * or single one:             $(image).lazyLoadXT()
     */
    $.fn.lazyLoadXT = function (selector) {
        selector = selector || options.selector;

        // stop call of queueCheckLazyElements->timeoutLazyElements by triggerEvent('init')
        waitingMode = 2;

        this.each(function () {
            if ('src' in this) {
                addElement(this);
            } else {
                if (this === window) {
                    $(selector).each(addElement);
                } else {
                    $(this)
                        .find(selector)
                        .each(addElement);
                }
            }
        });

        // run check of visibility
        waitingMode = 0;
        queueCheckLazyElements();
        return this;
    };

    /**
     * Initialize list of hidden elements
     */
    function initLazyElements() {
        $window.lazyLoadXT();
    }

    /**
     * Loading of all elements
     */
    function forceLoadAll() {
        checkLazyElements(true);
    }

    /**
     * Initialization
     */
    $(document).ready(function () {
        $window
            .on(options.loadEvent, initLazyElements)
            .on(options.updateEvent, queueCheckLazyElements)
            .on(options.forceEvent, forceLoadAll);

        if (options.autoInit) {
            initLazyElements(); // standard initialization
        }
    });

}(window.jQuery || window.Zepto, window, document));
<commit_msg>deferred run of timeoutLazyElements (to prevent possible loops)<commit_after>/*jslint browser:true, plusplus:true, vars:true */
/*jshint browser:true, jquery:true */
/*jshint -W040:false */ /* to don't alert on "this" in triggerLoad and triggerError */

(function ($, window, document) {
    'use strict';

    // options
    var options = {
            autoInit: true,
            selector: 'img',
            srcAttr: 'data-src',
            classNojs: 'lazy',
            blankImage: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
            edgeX: 0,
            edgeY: 0,
            throttle: 99,
            visibleOnly: true,
            loadEvent: 'pageshow', // check AJAX-loaded content in jQueryMobile
            updateEvent: 'load orientationchange resize scroll', // page-modified events
            forceEvent: '', // force loading of all elements
            oninit: null, // init handler
            onshow: null, // start loading handler
            onload: null, // load success handler
            onerror: null // error handler
        },
        $window = $(window),
        elements = [],
        viewportTop,
        viewportBottom,
        viewportLeft,
        viewportRight,
        topLazy = 0,
    /*
     waitingMode=0 : no setTimeout
     waitingMode=1 : setTimeout, no deferred events
     waitingMode=2 : setTimeout, deferred events
     */
        waitingMode = 0;

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

    /**
     * Process function/object event handler
     * @param {string} event suffix
     * @param {jQuery} $el
     */
    function triggerEvent(event, $el) {
        $el.trigger('lazy' + event);
        var handler = options['on' + event];
        if (handler) {
            if ($.isFunction(handler)) {
                handler.call($el[0]);
            } else {
                $el
                    .addClass(handler.addClass)
                    .removeClass(handler.removeClass);
            }
        }
        // queue next check as images may be resized after loading of actual file
        queueCheckLazyElements();
    }

    /**
     * Add element to lazy-load list
     * Call: addElement(idx, el) or addElement(el)
     * @param idx
     * @param {HTMLElement} [el]
     */
    function addElement(idx, el) {
        var $el = $(el || idx);

        // prevent duplicates
        if ($el.data('lazied')) {
            return;
        }
        $el
            .data('lazied', 1)
            .removeClass(options.classNojs);

        if (options.blankImage && $el[0].tagName === 'IMG' && !$el.attr('src')) {
            $el.attr('src', options.blankImage);
        }

        triggerEvent('init', $el);

        elements.unshift($el); // push it in the first position as we iterate elements in reverse order
    }

    /**
     * Save visible viewport boundary to viewportXXX variables
     */
    function calcViewport() {
        var scrollTop = $window.scrollTop(),
            scrollLeft = window.pageXOffset || 0,
            edgeX = options.edgeX,
            edgeY = options.edgeY;

        viewportTop = scrollTop - edgeY;
        viewportBottom = scrollTop + (window.innerHeight || $window.height()) + edgeY;
        viewportLeft = scrollLeft - edgeX;
        viewportRight = scrollLeft + (window.innerWidth || $window.width()) + edgeX;
    }

    /**
     * Trigger onload handler
     */
    function triggerLoad() {
        triggerEvent('load', $(this));
    }

    /**
     * Trigger onerror handler
     */
    function triggerError() {
        triggerEvent('error', $(this));
    }

    /**
     * Load visible elements
     * @param {bool} [force] loading of all elements
     */
    function checkLazyElements(force) {
        if (!elements.length) {
            return;
        }

        topLazy = Infinity;
        calcViewport();

        var i = elements.length - 1,
            srcAttr = options.srcAttr,
            isFuncSrcAttr = $.isFunction(srcAttr);
        for (; i >= 0; i--) {
            var $el = elements[i],
                el = $el[0];

            // remove items that are not in DOM
            if (!$.contains(document.body, el)) {
                elements.splice(i, 1);
            } else if (force || !options.visibleOnly || el.offsetWidth > 0 || el.offsetHeight > 0) {
                var offset = $el.offset(),
                    elTop = offset.top,
                    elLeft = offset.left;

                if (force ||
                    ((elTop < viewportBottom) && (elTop + $el.height() > viewportTop) &&
                        (elLeft < viewportRight) && (elLeft + $el.width() > viewportLeft))) {

                    triggerEvent('show', $el);

                    var src = isFuncSrcAttr ? srcAttr($el) : $el.attr(srcAttr);
                    if (src) {
                        $el
                            .on('load', triggerLoad)
                            .on('error', triggerError)
                            .attr('src', src);
                    }

                    elements.splice(i, 1);
                } else {
                    if (elTop < topLazy) {
                        topLazy = elTop;
                    }
                }
            }
        }
    }

    /**
     * Run check of lazy elements after timeout
     */
    function timeoutLazyElements() {
        if (waitingMode > 1) {
            waitingMode = 1;
            checkLazyElements();
            setTimeout(timeoutLazyElements, options.throttle);
        } else {
            waitingMode = 0;
        }
    }

    /**
     * Queue check of lazy elements because of event e
     * @param {Event} [e]
     */
    function queueCheckLazyElements(e) {
        if (!elements.length) {
            return;
        }

        // fast check for scroll event without new visible elements
        if (e && e.type === 'scroll') {
            calcViewport();
            if (topLazy >= viewportBottom) {
                return;
            }
        }

        if (!waitingMode) {
            waitingMode = 2;
            setTimeout(timeoutLazyElements, 0);
        } else {
            waitingMode = 2;
        }
    }

    /**
     * Add batch of new elements: $(container).lazyLoadXT([optional selector])
     * or single one:             $(image).lazyLoadXT()
     */
    $.fn.lazyLoadXT = function (selector) {
        selector = selector || options.selector;

        this.each(function () {
            if ('src' in this) {
                addElement(this);
            } else {
                if (this === window) {
                    $(selector).each(addElement);
                } else {
                    $(this)
                        .find(selector)
                        .each(addElement);
                }
            }
        });

        queueCheckLazyElements();
        return this;
    };

    /**
     * Initialize list of hidden elements
     */
    function initLazyElements() {
        $window.lazyLoadXT();
    }

    /**
     * Loading of all elements
     */
    function forceLoadAll() {
        checkLazyElements(true);
    }

    /**
     * Initialization
     */
    $(document).ready(function () {
        $window
            .on(options.loadEvent, initLazyElements)
            .on(options.updateEvent, queueCheckLazyElements)
            .on(options.forceEvent, forceLoadAll);

        if (options.autoInit) {
            initLazyElements(); // standard initialization
        }
    });

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


<commit_before>require("underscore");
require("es6-promise").polyfill();
require("@/common/polyfill");
require("jquery");

var utils = require("@/common/utils");
var commonConfig = require("@/common/config");
var uikit = require("../pages/main/uikit");
var api = require("./api");
var Selector = require("../pages/main/uikit/selector");
var getToHost = require("@/app/common/transfer");
var _const = require("@/common/const");

var isSending = false;

var dom = utils.createElementFromHTML([
	"
",
	"
",
	"
" + __("ticket.note_title") + "
",
	"
" + __("ticket.title") + "
",
	"<input type=\"text\" class=\"name\" placeholder=\"" + __("ticket.name") + "\"> *",
	"<input type=\"text\" class=\"phone\" placeholder=\"" + __("ticket.phone_number") + "\"> *",
	"<input type=\"text\" class=\"mail\" placeholder=\"" + __("ticket.email") + "\">",
	"

",
	"<textarea spellcheck=\"false\" placeholder=\"" + __("ticket.content_placeholder") + "\"></textarea>*",
	"

",
	"
" + __("common.cancel") + "
",
	"
"+ __("common.ticket") +"
",
	"
",
	"
"
].join(""));

if(utils.isMobile  || ($("body",parent.document).hasClass("window-demo") && $("#em-kefu-webim-self",parent.document).hasClass("hide"))){
	var dom = utils.createElementFromHTML([
		"
",
		"
",
		"
" + __("ticket.note_title") + "
",
		"
" + __("ticket.title") + "
",
		"<input type=\"text\" class=\"name\" placeholder=\"" + __("ticket.name") + "\"> *",
		"<input type=\"text\" class=\"phone\" placeholder=\"" + __("ticket.phone_number") + "\"> *",
		"<input type=\"text\" class=\"mail\" placeholder=\"" + __("ticket.email") + "\">",
		"

",
		"<textarea spellcheck=\"false\" placeholder=\"" + __("ticket.content_placeholder") + "\"></textarea>*",
		"

",
		"
" + __("common.cancel") + "
",
		"
"+ __("common.ticket") +"
",
		"
",
		"
"
	].join(""));
}
else if($("body",parent.document).hasClass("window-demo") && !$("#em-kefu-webim-self",parent.document).hasClass("hide")){
	var dom = utils.createElementFromHTML([
		"
",
		"
",
		"
" + __("ticket.note_title") + "
",
		"
" + __("ticket.title") + "
",
		"<input type=\"text\" class=\"name\" placeholder=\"" + __("ticket.name") + "\"> *",
		"<input type=\"text\" class=\"phone\" placeholder=\"" + __("ticket.phone_number") + "\"> *",
		"<input type=\"text\" class=\"mail\" placeholder=\"" + __("ticket.email") + "\">",
		"

",
		"<textarea spellcheck=\"false\" placeholder=\"" + __("ticket.content_placeholder") + "\"></textarea>*",
		"

",
		"
" + __("common.cancel") + "
",
		"
"+ __("common.ticket") +"
",
		"
",
		"
"
	].join(""));
}
document.body.appendChild(dom);
console.log(config, 'offDutyType+++')
if( $("body",parent.document).hasClass("window-demo") && config.offDutyType == ""){
	console.log("下班时间隐藏留言" )
	dom.addClass(hide);
	getToHost.listen(function(msg){
		var event = msg.event;

		switch(event){
		case _const.EVENTS.SHOW:
			console.log("收到点击消息，展开留言" )
			dom.removeClass(hide);
		case _const.EVENTS.CLOSE:

			break;
		default:
			break;
		}
	}, ["down2Im"]);
}

var content = document.querySelector("textarea");
var name = document.querySelector(".name");
var phone = document.querySelector(".phone");
var mail = document.querySelector(".mail");
var noteCategory = document.querySelector(".note-category");
var noteCategoryList = new Selector({
	list: [],
	container: noteCategory,
});
// var cancelBtn = document.querySelector(".cancel-btn");
// var confirmBtn = document.querySelector(".confirm-btn");
var cancelBtn = document.querySelector(".cancel");
var confirmBtn = document.querySelector(".confirm");
// url 传递的参数
var config = getNoteConfig().config || {};
console.log(config,"offDutyType++" )
api.update(config);
utils.addClass(document.body, config.themeClassName || "theme-1");
// 根据配置隐藏取消按钮
config.hideCloseBtn && utils.addClass(cancelBtn, "hide");

var themeCustomColor = config.themeCustomColor;
if(themeCustomColor){
	// color = config.themeColor;
	$(".theme_custom").find(".bg-color").css("cssText","background-color: " + themeCustomColor + " !important");
}
var color = config.themeColor;
if(color){
	$(".theme_custom").find(".bg-color").css("cssText","background-color: " + color + " !important");
}
if(config.offDutyType == ""){
	var closeEl = $(".ticket>.wrapper .icon-close");
	var ticketEl = $(".ticket>.wrapper .confirm");
	closeEl&&closeEl.remove();
	$(ticketEl).css("cssText","margin-left: 280px;");
}

var _getCategories = _.once(function(){
	api.getNoteCategories().then(function(list){
		var optionList = _.map(list, function(item){
			return {
				sign: item.id,
				desc: item.name
			};
		});

		if(!_.isEmpty(optionList)){
			utils.removeClass(noteCategory, "hide");
			noteCategoryList.updateList({ list: optionList });
		}
	});
});

function getNoteConfig(){
	var i;
	var len;
	var tmp = [];
	var obj = {};
	// 需要测兼容性
	var src = document.location.search;
	var arr = src.slice(1).split("&");
	for(i = 0, len = arr.length; i < len; i++){
		tmp = arr[i].split("=");
		tmp[1] = window.atob
			? window.atob(tmp[1])
			: tmp[1];
		obj[tmp[0]] = tmp.length > 1
			? JSON.parse(decodeURIComponent(tmp[1]))
			: "";
	}
	return obj;
}

function _createTicket(){
	Promise.all([
		api.getToken(),
		api.getProjectId()
	]).then(function(result){
		var token = result[0];
		var projectId = result[1];
		var sessionId = config.sessionId || "";

		api.createTicket({
			token: token,
			projectId: projectId,
			name: name.value,
			phone: phone.value,
			mail: mail.value,
			content: content.value,
			category_id: noteCategoryList.getSelectedValue(),
			session_id: sessionId,
		}).then(function(){
			isSending = false;
			uikit.showSuccess(__("ticket.send_success"));

			_clearInput();
		}, function(err){
			isSending = false;
			uikit.tip(__("ticket.send_failed_retry"));
			console.error(err);
		});
	})
	["catch"](function(err){
		uikit.tip(__("ticket.send_failed_invalid_token"));
		console.error(err);
	});
}

function _clearInput(){
	name.value = "";
	phone.value = "";
	mail.value = "";
	content.value = "";
}

function _writePreDate(preData){
	name.value = preData.name || "";
	phone.value = preData.phone || "";
	mail.value = preData.mail || "";
}

config.grayNoteCategory && _getCategories();
config.preData && _writePreDate(config.preData);

// 海外用户，不验证手机号格式 新增敦煌用户不校验格式2020-05-06
function overseasTest(){
	if(config.tenantId == "76141"){
		return !phone.value;
	}
	if(config.tenantId == "78882"){
		return !phone.value;
	}
	if(config.tenantId == "6437"){
        return !phone.value;
    }
	return !phone.value || !(/^1[3456789]\d{9}$/.test(phone.value));
}
// 添加邮箱的校验
function checkEmail(){
	var check = /^[0-9a-z]([_.0-9a-z-]{0,30}[0-9a-z])?@([0-9a-z][0-9a-z-]{0,30}[.]){1,3}[a-z]{2,4}$/i;
	if(mail.value){
		if(check.test(mail.value)){
			return true
		}else{
			return false
		}
	}
	else{
		return true
	}
}

// 留言
utils.on(confirmBtn, "click", function(){
	if(isSending){
		uikit.tip(__("ticket.is_sending"));
	}
	else if(!name.value || name.value.length > 140){
		uikit.tip(__("ticket.invalid_name"));
	}
	else if(overseasTest()){
		uikit.tip(__("ticket.invalid_phone"));
	}
	else if(!checkEmail()){
		uikit.tip(__("ticket.invalid_email"));
	}
	else if(!content.value || content.value.length > 1500){
		uikit.tip(__("ticket.invalid_content"));
	}
	else{
		isSending = true;
		setTimeout(function(){ isSending = false; }, 10000);
		_createTicket();
	}
});

// 取消
utils.on(cancelBtn, "click", function(){
	if(config.offDutyType == ""){

	}
	else{
		window.parent.postMessage({ closeNote: true }, "*");
	}
});
utils.live(".wrapper-title .icon-close","click",function(){
	// window.parent.postMessage({ closeNote: true }, "*");
	if(config.offDutyType == ""){

	}
	else{
		window.parent.postMessage({ closeNote: true }, "*");
	}
});
utils.live(".wrapper-title .icon-back-new","click",function(){
	// window.parent.postMessage({ closeNote: true }, "*");
	if(config.offDutyType == ""){

	}
	else{
		window.parent.postMessage({ closeNote: true }, "*");
	}
});
<commit_msg>下班留言样式<commit_after>require("underscore");
require("es6-promise").polyfill();
require("@/common/polyfill");
require("jquery");

var utils = require("@/common/utils");
var commonConfig = require("@/common/config");
var uikit = require("../pages/main/uikit");
var api = require("./api");
var Selector = require("../pages/main/uikit/selector");
var getToHost = require("@/app/common/transfer");
var _const = require("@/common/const");

var isSending = false;

var dom = utils.createElementFromHTML([
	"
",
	"
",
	"
" + __("ticket.note_title") + "
",
	"
" + __("ticket.title") + "
",
	"<input type=\"text\" class=\"name\" placeholder=\"" + __("ticket.name") + "\"> *",
	"<input type=\"text\" class=\"phone\" placeholder=\"" + __("ticket.phone_number") + "\"> *",
	"<input type=\"text\" class=\"mail\" placeholder=\"" + __("ticket.email") + "\">",
	"

",
	"<textarea spellcheck=\"false\" placeholder=\"" + __("ticket.content_placeholder") + "\"></textarea>*",
	"

",
	"
" + __("common.cancel") + "
",
	"
"+ __("common.ticket") +"
",
	"
",
	"
"
].join(""));

if(utils.isMobile  || ($("body",parent.document).hasClass("window-demo") && $("#em-kefu-webim-self",parent.document).hasClass("hide"))){
	var dom = utils.createElementFromHTML([
		"
",
		"
",
		"
" + __("ticket.note_title") + "
",
		"
" + __("ticket.title") + "
",
		"<input type=\"text\" class=\"name\" placeholder=\"" + __("ticket.name") + "\"> *",
		"<input type=\"text\" class=\"phone\" placeholder=\"" + __("ticket.phone_number") + "\"> *",
		"<input type=\"text\" class=\"mail\" placeholder=\"" + __("ticket.email") + "\">",
		"

",
		"<textarea spellcheck=\"false\" placeholder=\"" + __("ticket.content_placeholder") + "\"></textarea>*",
		"

",
		"
" + __("common.cancel") + "
",
		"
"+ __("common.ticket") +"
",
		"
",
		"
"
	].join(""));
}
else if($("body",parent.document).hasClass("window-demo") && !$("#em-kefu-webim-self",parent.document).hasClass("hide")){
	var dom = utils.createElementFromHTML([
		"
",
		"
",
		"
" + __("ticket.note_title") + "
",
		"
" + __("ticket.title") + "
",
		"<input type=\"text\" class=\"name\" placeholder=\"" + __("ticket.name") + "\"> *",
		"<input type=\"text\" class=\"phone\" placeholder=\"" + __("ticket.phone_number") + "\"> *",
		"<input type=\"text\" class=\"mail\" placeholder=\"" + __("ticket.email") + "\">",
		"

",
		"<textarea spellcheck=\"false\" placeholder=\"" + __("ticket.content_placeholder") + "\"></textarea>*",
		"

",
		"
" + __("common.cancel") + "
",
		"
"+ __("common.ticket") +"
",
		"
",
		"
"
	].join(""));
}
document.body.appendChild(dom);
console.log(config, 'offDutyType+++')
if( $("body",parent.document).hasClass("window-demo") && config.offDutyType == ""){
	console.log("下班时间隐藏留言" )
	$(dom).addClass("hide");
	getToHost.listen(function(msg){
		var event = msg.event;

		switch(event){
		case _const.EVENTS.SHOW:
			console.log("收到点击消息，展开留言" )
			$(dom).removeClass("hide");
		case _const.EVENTS.CLOSE:

			break;
		default:
			break;
		}
	}, ["down2Im"]);
}

var content = document.querySelector("textarea");
var name = document.querySelector(".name");
var phone = document.querySelector(".phone");
var mail = document.querySelector(".mail");
var noteCategory = document.querySelector(".note-category");
var noteCategoryList = new Selector({
	list: [],
	container: noteCategory,
});
// var cancelBtn = document.querySelector(".cancel-btn");
// var confirmBtn = document.querySelector(".confirm-btn");
var cancelBtn = document.querySelector(".cancel");
var confirmBtn = document.querySelector(".confirm");
// url 传递的参数
var config = getNoteConfig().config || {};
console.log(config,"offDutyType++" )
api.update(config);
utils.addClass(document.body, config.themeClassName || "theme-1");
// 根据配置隐藏取消按钮
config.hideCloseBtn && utils.addClass(cancelBtn, "hide");

var themeCustomColor = config.themeCustomColor;
if(themeCustomColor){
	// color = config.themeColor;
	$(".theme_custom").find(".bg-color").css("cssText","background-color: " + themeCustomColor + " !important");
}
var color = config.themeColor;
if(color){
	$(".theme_custom").find(".bg-color").css("cssText","background-color: " + color + " !important");
}
if(config.offDutyType == ""){
	var closeEl = $(".ticket>.wrapper .icon-close");
	var ticketEl = $(".ticket>.wrapper .confirm");
	closeEl&&closeEl.remove();
	$(ticketEl).css("cssText","margin-left: 280px;");
}

var _getCategories = _.once(function(){
	api.getNoteCategories().then(function(list){
		var optionList = _.map(list, function(item){
			return {
				sign: item.id,
				desc: item.name
			};
		});

		if(!_.isEmpty(optionList)){
			utils.removeClass(noteCategory, "hide");
			noteCategoryList.updateList({ list: optionList });
		}
	});
});

function getNoteConfig(){
	var i;
	var len;
	var tmp = [];
	var obj = {};
	// 需要测兼容性
	var src = document.location.search;
	var arr = src.slice(1).split("&");
	for(i = 0, len = arr.length; i < len; i++){
		tmp = arr[i].split("=");
		tmp[1] = window.atob
			? window.atob(tmp[1])
			: tmp[1];
		obj[tmp[0]] = tmp.length > 1
			? JSON.parse(decodeURIComponent(tmp[1]))
			: "";
	}
	return obj;
}

function _createTicket(){
	Promise.all([
		api.getToken(),
		api.getProjectId()
	]).then(function(result){
		var token = result[0];
		var projectId = result[1];
		var sessionId = config.sessionId || "";

		api.createTicket({
			token: token,
			projectId: projectId,
			name: name.value,
			phone: phone.value,
			mail: mail.value,
			content: content.value,
			category_id: noteCategoryList.getSelectedValue(),
			session_id: sessionId,
		}).then(function(){
			isSending = false;
			uikit.showSuccess(__("ticket.send_success"));

			_clearInput();
		}, function(err){
			isSending = false;
			uikit.tip(__("ticket.send_failed_retry"));
			console.error(err);
		});
	})
	["catch"](function(err){
		uikit.tip(__("ticket.send_failed_invalid_token"));
		console.error(err);
	});
}

function _clearInput(){
	name.value = "";
	phone.value = "";
	mail.value = "";
	content.value = "";
}

function _writePreDate(preData){
	name.value = preData.name || "";
	phone.value = preData.phone || "";
	mail.value = preData.mail || "";
}

config.grayNoteCategory && _getCategories();
config.preData && _writePreDate(config.preData);

// 海外用户，不验证手机号格式 新增敦煌用户不校验格式2020-05-06
function overseasTest(){
	if(config.tenantId == "76141"){
		return !phone.value;
	}
	if(config.tenantId == "78882"){
		return !phone.value;
	}
	if(config.tenantId == "6437"){
        return !phone.value;
    }
	return !phone.value || !(/^1[3456789]\d{9}$/.test(phone.value));
}
// 添加邮箱的校验
function checkEmail(){
	var check = /^[0-9a-z]([_.0-9a-z-]{0,30}[0-9a-z])?@([0-9a-z][0-9a-z-]{0,30}[.]){1,3}[a-z]{2,4}$/i;
	if(mail.value){
		if(check.test(mail.value)){
			return true
		}else{
			return false
		}
	}
	else{
		return true
	}
}

// 留言
utils.on(confirmBtn, "click", function(){
	if(isSending){
		uikit.tip(__("ticket.is_sending"));
	}
	else if(!name.value || name.value.length > 140){
		uikit.tip(__("ticket.invalid_name"));
	}
	else if(overseasTest()){
		uikit.tip(__("ticket.invalid_phone"));
	}
	else if(!checkEmail()){
		uikit.tip(__("ticket.invalid_email"));
	}
	else if(!content.value || content.value.length > 1500){
		uikit.tip(__("ticket.invalid_content"));
	}
	else{
		isSending = true;
		setTimeout(function(){ isSending = false; }, 10000);
		_createTicket();
	}
});

// 取消
utils.on(cancelBtn, "click", function(){
	if(config.offDutyType == ""){

	}
	else{
		window.parent.postMessage({ closeNote: true }, "*");
	}
});
utils.live(".wrapper-title .icon-close","click",function(){
	// window.parent.postMessage({ closeNote: true }, "*");
	if(config.offDutyType == ""){

	}
	else{
		window.parent.postMessage({ closeNote: true }, "*");
	}
});
utils.live(".wrapper-title .icon-back-new","click",function(){
	// window.parent.postMessage({ closeNote: true }, "*");
	if(config.offDutyType == ""){

	}
	else{
		window.parent.postMessage({ closeNote: true }, "*");
	}
});
<|endoftext|>


<commit_before>'use strict';

angular.module('myApp.query_crosstab', ['ngRoute', 'ServicesModule', 'ngDialog'])

.config(['$routeProvider', 'ngDialogProvider', '$locationProvider', function($routeProvider, ngDialogProvider, $locationProvider) {
  $routeProvider.when('/query_crosstab', {
    templateUrl: 'static/query_crosstab/query_crosstab.html',
    controller: 'QueryCrosstabCtrl'
  });

  ngDialogProvider.setDefaults({
    className: 'ngdialog-theme-plain',
    showClose: true,
    closeByDocument: true,
    closeByEscape: true
  });

  $locationProvider.html5Mode(true);
}])

.controller('QueryCrosstabCtrl', ['$scope', '$http', 'ExcelService', 'ngDialog',
    function($scope, $http, ExcelService, ngDialog) {
      $scope.queryAttributes = [];

      $scope.init = function() {
        $http.get('/getAttributesList/').then(function(res){
            $scope.queryAttributes = res.data;
        });
      };

      $scope.searchButtonText = 'Query!';
      $scope.showCrossTab = false;
      $scope.isQuerying = false;
      $scope.returnedError = false;
      $scope.returnedErrorMessage = '';
      $scope.bhds_total = 0;
      $scope.fwm_total = 0;

      $scope.chosenAttributes = [];
      $scope.submittedAttributes = [];
      $scope.crossTabsAttributes = [];

      $scope.onDragComplete=function(data,evt){
      };

      $scope.onDropComplete=function(data,evt){
        $scope.chosenAttributes.push(data);
      };

      $scope.removeQueryAttr = function(data){
        angular.forEach($scope.chosenAttributes, function(value, key)
        {
           if(data.name == value.name){
               $scope.chosenAttributes.splice(key, 1);
               return;
           }
        });
      };

      $scope.search = function(){
        $scope.showCrossTab = false;
        $scope.isQuerying = true;
        $scope.returnedError = false;
        $scope.returnedErrorMessage = '';
        $scope.searchButtonText = "Querying!";
        $scope.submittedAttributes = angular.copy($scope.chosenAttributes);

        ngDialog.open({
            template:'static/partials/crosstab.html',
            scope: $scope
        });

        $http.post('/queryHive/',{chosenAttributes: $scope.submittedAttributes})
        .then(function(res){
            buildCrossTabsAttributes(res.data);
            $scope.isQuerying = false;
            $scope.showCrossTab = true;
            $scope.searchButtonText = 'Query!';
        },function(res){
            $scope.searchButtonText = 'Query!';
            $scope.isQuerying = false;
            $scope.returnedError = true;
            $scope.returnedErrorMessage = res.data;
        });
      };

      $scope.getAssociatedAttribute = function(baseAttribute, associatedIndex)
      {
          var associatedAttribute = {};
          if(baseAttribute.name == $scope.crossTabsAttributes[associatedIndex].name)
          {
              associatedAttribute = angular.copy(baseAttribute);
              associatedAttribute.bhds_percent = 1.0
              associatedAttribute.fwm_percent = 1.0
          }else{
              var mergedAttribute = {}

              $scope.crossTabsAttributes.map(function(obj){
                if(obj.name == baseAttribute.name)
                {
                    mergedAttribute = angular.copy(obj);
                    return;
                }
              });

              mergedAttribute.joins.map(function(obj){
                  if(obj.name == $scope.crossTabsAttributes[associatedIndex].name){
                      associatedAttribute = obj;
                  }
              });
          }

          return associatedAttribute;
      }

      var buildCrossTabsAttributes = function(totals){
        $scope.crossTabsAttributes = [];
        $scope.bhds_total = totals.total_bhds;
        $scope.fwm_total = totals.total_fwm;

        $scope.submittedAttributes.map(function(obj){
            var attrObj = angular.copy(obj);

            attrObj.bhds_total = totals['total_' + attrObj.id];
            attrObj.bhds_percent = parseInt(attrObj.bhds_total) / parseInt($scope.bhds_total);
            attrObj.fwm_total = totals['total_' + attrObj.id + "_fwm"];
            attrObj.fwm_percent = parseInt(attrObj.fwm_total) / parseInt($scope.fwm_total);

            attrObj.joins = [];

            $scope.submittedAttributes.map(function(obj2){
                if(obj.id != obj2.id)
                {
                    var joinObj = angular.copy(obj2);

                    if(parseInt(attrObj.id) < parseInt(obj2.id))
                    {
                        var id1 = attrObj.id;
                        var id2 = obj2.id;
                    }else{
                        var id1 = obj2.id;
                        var id2 = attrObj.id;
                    }

                    joinObj.bhds_total = totals['total_' + id1 + '_' + id2];
                    joinObj.bhds_percent = parseInt(joinObj.bhds_total) / parseInt(totals['total_' + obj2.id]);
                    joinObj.fwm_total = totals['total_' + id1 + '_' + id2 + '_fwm'];
                    joinObj.fwm_percent = parseInt(joinObj.fwm_total) / parseInt(totals['total_' + obj2.id + '_fwm']);

                    attrObj.joins.push(joinObj);
                }
            });

            $scope.crossTabsAttributes.push(attrObj);
        });
      }

      $scope.clear = function() {
        $scope.chosenAttributes = [];
      };

      $scope.exportToExcel=function(tableId){
        ExcelService.tableToExcel(tableId, 'Crosstab');
      };

      $scope.init();
}]);<commit_msg>Fix bug that caused NaN% and empty fields in crosstab table -Change logic to use string ids rather than integer ids<commit_after>'use strict';

angular.module('myApp.query_crosstab', ['ngRoute', 'ServicesModule', 'ngDialog'])

.config(['$routeProvider', 'ngDialogProvider', '$locationProvider', function($routeProvider, ngDialogProvider, $locationProvider) {
  $routeProvider.when('/query_crosstab', {
    templateUrl: 'static/query_crosstab/query_crosstab.html',
    controller: 'QueryCrosstabCtrl'
  });

  ngDialogProvider.setDefaults({
    className: 'ngdialog-theme-plain',
    showClose: true,
    closeByDocument: true,
    closeByEscape: true
  });

  $locationProvider.html5Mode(true);
}])

.controller('QueryCrosstabCtrl', ['$scope', '$http', 'ExcelService', 'ngDialog',
    function($scope, $http, ExcelService, ngDialog) {
      $scope.queryAttributes = [];

      $scope.init = function() {
        $http.get('/getAttributesList/').then(function(res){
            $scope.queryAttributes = res.data;
        });
      };

      $scope.searchButtonText = 'Query!';
      $scope.showCrossTab = false;
      $scope.isQuerying = false;
      $scope.returnedError = false;
      $scope.returnedErrorMessage = '';
      $scope.bhds_total = 0;
      $scope.fwm_total = 0;

      $scope.chosenAttributes = [];
      $scope.submittedAttributes = [];
      $scope.crossTabsAttributes = [];

      $scope.onDragComplete=function(data,evt){
      };

      $scope.onDropComplete=function(data,evt){
        $scope.chosenAttributes.push(data);
      };

      $scope.removeQueryAttr = function(data){
        angular.forEach($scope.chosenAttributes, function(value, key)
        {
           if(data.name == value.name){
               $scope.chosenAttributes.splice(key, 1);
               return;
           }
        });
      };

      $scope.search = function(){
        $scope.showCrossTab = false;
        $scope.isQuerying = true;
        $scope.returnedError = false;
        $scope.returnedErrorMessage = '';
        $scope.searchButtonText = "Querying!";
        $scope.submittedAttributes = angular.copy($scope.chosenAttributes);

        ngDialog.open({
            template:'static/partials/crosstab.html',
            scope: $scope
        });

        $http.post('/queryHive/',{chosenAttributes: $scope.submittedAttributes})
        .then(function(res){
            buildCrossTabsAttributes(res.data);
            $scope.isQuerying = false;
            $scope.showCrossTab = true;
            $scope.searchButtonText = 'Query!';
        },function(res){
            $scope.searchButtonText = 'Query!';
            $scope.isQuerying = false;
            $scope.returnedError = true;
            $scope.returnedErrorMessage = res.data;
        });
      };

      $scope.getAssociatedAttribute = function(baseAttribute, associatedIndex)
      {
          var associatedAttribute = {};
          if(baseAttribute.name == $scope.crossTabsAttributes[associatedIndex].name)
          {
              associatedAttribute = angular.copy(baseAttribute);
              associatedAttribute.bhds_percent = 1.0
              associatedAttribute.fwm_percent = 1.0
          }else{
              var mergedAttribute = {}

              $scope.crossTabsAttributes.map(function(obj){
                if(obj.name == baseAttribute.name)
                {
                    mergedAttribute = angular.copy(obj);
                    return;
                }
              });

              mergedAttribute.joins.map(function(obj){
                  if(obj.name == $scope.crossTabsAttributes[associatedIndex].name){
                      associatedAttribute = obj;
                  }
              });
          }

          return associatedAttribute;
      }

      var buildCrossTabsAttributes = function(totals){
        $scope.crossTabsAttributes = [];
        $scope.bhds_total = totals.total_bhds;
        $scope.fwm_total = totals.total_fwm;

        $scope.submittedAttributes.map(function(obj){
            var attrObj = angular.copy(obj);

            attrObj.bhds_total = totals['total_' + attrObj.id];
            attrObj.bhds_percent = parseInt(attrObj.bhds_total) / parseInt($scope.bhds_total);
            attrObj.fwm_total = totals['total_' + attrObj.id + "_fwm"];
            attrObj.fwm_percent = parseInt(attrObj.fwm_total) / parseInt($scope.fwm_total);

            attrObj.joins = [];

            $scope.submittedAttributes.map(function(obj2){
                if(obj.id != obj2.id)
                {
                    var joinObj = angular.copy(obj2);

                    if(totals.hasOwnProperty('total_' + attrObj.id + '_' + obj2.id))
                    {
                        joinObj.bhds_total = totals['total_' + attrObj.id + '_' + obj2.id];
                        joinObj.fwm_total = totals['total_' + attrObj.id + '_' + obj2.id + '_fwm'];
                    }else{
                        joinObj.bhds_total = totals['total_' + obj2.id + '_' + attrObj.id];
                        joinObj.fwm_total = totals['total_' + obj2.id + '_' + attrObj.id + '_fwm'];
                    }

                    joinObj.bhds_percent = parseInt(joinObj.bhds_total) / parseInt(totals['total_' + obj2.id]);
                    joinObj.fwm_percent = parseInt(joinObj.fwm_total) / parseInt(totals['total_' + obj2.id + '_fwm']);

                    attrObj.joins.push(joinObj);
                }
            });

            $scope.crossTabsAttributes.push(attrObj);
        });
      }

      $scope.clear = function() {
        $scope.chosenAttributes = [];
      };

      $scope.exportToExcel=function(tableId){
        ExcelService.tableToExcel(tableId, 'Crosstab');
      };

      $scope.init();
}]);<|endoftext|>


<commit_before>/**
<<<<<<< HEAD
* @fileOverview Requirejs module containing base antie.devices.logging.onscreen class.
*
* @preserve Copyright (c) 2013 British Broadcasting Corporation
* (http://www.bbc.co.uk) and TAL Contributors (1)
*
* (1) TAL Contributors are listed in the AUTHORS file and at
*     https://github.com/fmtvp/TAL/AUTHORS - please extend this file,
*     not this notice.
*
* @license 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.
*
* All rights reserved
* Please contact us for an alternative licence
*/

// Saves the logs for access later
require.def(
	'antie/devices/logging/saving',
	[ 'module', 'antie/devices/device', 'antie/application' ],
	function(Module, Device, Application)
	{
		var logItems = [];

		var saveData = function (type, data) {
			var cleanedData = [];
			for (var i = 0; i < data.length; ++i) {
				cleanedData.push(data[i] + '');
			}

			logItems.push({ 'level': type, 'message': cleanedData })
		};

		var loggingMethods = {
			log: function() {
				saveData("LOG", arguments);
			},
			debug: function() {
				saveData("DEBUG", arguments);
			},
			info: function() {
				saveData("INFO", arguments);
			},
			warn: function() {
				saveData("WARN", arguments);
			},
			error: function() {
				saveData("ERROR", arguments);
			}
		};

		Device.addLoggingStrategy( Module.id, loggingMethods );
		return { getLogItems: function () {
			var data = logItems.slice(0);
			logItems = [];
			return data;
		}};
	}
);
<commit_msg>Fix merge failure<commit_after>/**
* @fileOverview Requirejs module containing base antie.devices.logging.onscreen class.
*
* @preserve Copyright (c) 2013 British Broadcasting Corporation
* (http://www.bbc.co.uk) and TAL Contributors (1)
*
* (1) TAL Contributors are listed in the AUTHORS file and at
*     https://github.com/fmtvp/TAL/AUTHORS - please extend this file,
*     not this notice.
*
* @license 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.
*
* All rights reserved
* Please contact us for an alternative licence
*/

// Saves the logs for access later
require.def(
	'antie/devices/logging/saving',
	[ 'module', 'antie/devices/device', 'antie/application' ],
	function(Module, Device, Application)
	{
		var logItems = [];

		var saveData = function (type, data) {
			var cleanedData = [];
			for (var i = 0; i < data.length; ++i) {
				cleanedData.push(data[i] + '');
			}

			logItems.push({ 'level': type, 'message': cleanedData })
		};

		var loggingMethods = {
			log: function() {
				saveData("LOG", arguments);
			},
			debug: function() {
				saveData("DEBUG", arguments);
			},
			info: function() {
				saveData("INFO", arguments);
			},
			warn: function() {
				saveData("WARN", arguments);
			},
			error: function() {
				saveData("ERROR", arguments);
			}
		};

		Device.addLoggingStrategy( Module.id, loggingMethods );
		return { getLogItems: function () {
			var data = logItems.slice(0);
			logItems = [];
			return data;
		}};
	}
);
<|endoftext|>