<commit_before>/**
 * @class CM_Form_Abstract
 * @extends CM_View_Abstract
 */
var CM_Form_Abstract = CM_View_Abstract.extend({
  _class: 'CM_Form_Abstract',

  /** @type Object **/
  _fields: {},

  /** @type Object **/
  _actions: {},

  /** @type Boolean **/
  _stopErrorPropagation: false,

  initialize: function() {
    CM_View_Abstract.prototype.initialize.call(this);

    this._fields = {};
    this._actions = {};
  },

  events: {
    'reset': function() {
      _.each(this._fields, function(field) {
        field.reset();
      });
      this.resetErrors();
    }
  },

  ready: function() {
  },

  _ready: function() {
    var handler = this;

    _.each(this._actions, function(action, name) {
      var $btn = $('#' + this.getAutoId() + '-' + name + '-button');
      var event = $btn.data('event');
      if (!event) {
        event = 'click';
      }
      $btn.on(event, {action: name}, function(event) {
        event.preventDefault();
        event.stopPropagation();
        return handler.submit(event.data.action);
      });
    }, this);

    this.$el.on('submit', function() {
      handler.$el.find('input[type="submit"], button[type="submit"]').first().click();
      return false;
    });

    CM_View_Abstract.prototype._ready.call(this);
  },

  /**
   * @param {CM_FormField_Abstract} field
   */
  registerField: function(field) {
    this._fields[field.getName()] = field;

    field.on('change', function() {
      this.trigger('change');
    }, this);
  },

  /**
   * @param {String} name
   * @param {Object} presentation
   */
  registerAction: function(name, presentation) {
    this._actions[name] = presentation;
  },

  /**
   * @return CM_FormField_Abstract
   */
  getField: function(name) {
    if (!this._fields[name]) {
      cm.error.triggerThrow(this.getClass() + ' cannot find form field `' + name + '`');
    }
    return this._fields[name];
  },

  /**
   * @return jQuery
   */
  $: function(selector) {
    if (!selector) {
      return this.$el;
    }
    selector = selector.replace('#', '#' + this.getAutoId() + '-');
    return $(selector, this.el);
  },

  /**
   * @param {String|Null} [actionName]
   */
  getData: function(actionName) {
    var form_data = this.$().serializeArray();
    var action = actionName ? this._actions[actionName] : null;

    var data = {};
    var regex = /^([\w\-]+)(\[([^\]]+)?\])?$/;
    var name, match;

    for (var i = 0, item; item = form_data[i]; i++) {
      match = regex.exec(item.name);
      name = match[1];
      item.value = item.value || '';

      if (action && typeof action.fields[name] == 'undefined') {
        continue;
      }

      if (!match[2]) {
        // Scalar
        data[name] = item.value;
      } else if (match[2] == '[]') {
        // Array
        if (typeof data[name] == 'undefined') {
          data[name] = [];
        }
        data[name].push(item.value);
      } else if (match[3]) {
        // Associative array
        if (typeof data[name] == 'undefined') {
          data[name] = {};
        }
        data[name][match[3]] = item.value;
      }
    }

    return data;
  },

  /**
   * @param {String} [actionName]
   * @param {Object} [options]
   * @return Promise
   */
  submit: function(actionName, options) {
    options = _.defaults(options || {}, {
      handleErrors: true,
      disableUI: true
    });
    var action = this._getAction(actionName);
    var data = this.getData(action.name);
    var deferred = $.Deferred();

    var errorList = this._getErrorList(action.name);

    if (options.handleErrors) {
      _.each(this._fields, function(field, fieldName) {
        if (errorList[fieldName]) {
          field.error(errorList[fieldName]);
        } else {
          field.error(null);
        }
      }, this);
    }

    if (_.size(errorList)) {
      deferred.reject();

    } else {
      if (options.disableUI) {
        this.disable();
      }
      this.trigger('submit', [data]);

      var handler = this;
      cm.ajax('form', {viewInfoList: this.getViewInfoList(), actionName: action.name, data: data}, {
        success: function(response) {
          if (response.errors) {
            if (options.handleErrors) {
              for (var i = response.errors.length - 1, error; error = response.errors[i]; i--) {
                if (_.isArray(error)) {
                  handler.getField(error[1]).error(error[0]);
                } else {
                  handler.error(error);
                }
              }
            }

            handler.trigger('error error.' + action.name);
            deferred.reject();
          }

          if (response.exec) {
            handler.evaluation = new Function(response.exec);
            handler.evaluation();
          }

          if (response.messages) {
            for (var i = 0, msg; msg = response.messages[i]; i++) {
              handler.message(msg);
            }
          }

          if (!response.errors) {
            handler.trigger('success success.' + action.name, response.data);
            deferred.resolve(response.data);
          }
        },
        error: function(msg, type, isPublic) {
          handler._stopErrorPropagation = false;
          handler.trigger('error error.' + action.name, msg, type, isPublic);
          deferred.reject();
          return !handler._stopErrorPropagation;
        },
        complete: function() {
          if (options.disableUI) {
            handler.enable();
          }
          handler.trigger('complete');
        }
      });
    }

    return deferred.promise();
  },

  stopErrorPropagation: function() {
    this._stopErrorPropagation = true;
  },

  reset: function() {
    this.el.reset();
  },

  disable: function() {
    this.$().disable();
  },

  enable: function() {
    this.$().enable();
  },

  /**
   * @param {String} message
   */
  error: function(message) {
    cm.window.hint(message);
  },

  /**
   * @param {String} message
   */
  message: function(message) {
    cm.window.hint(message);
  },

  resetErrors: function() {
    _.each(this._fields, function(field) {
      field.error(null);
    });
  },

  /**
   * @param {String} [actionName]
   * @returns {Object}
   */
  _getAction: function(actionName) {
    actionName = actionName || _.first(_.keys(this._actions));
    var action = this._actions[actionName];
    if (!action) {
      cm.error.triggerThrow('Form `' + this.getClass() + '` has no action `' + actionName + '`.');
    }
    action.name = actionName;
    return action;
  },

  /**
   * @param {String} [actionName]
   * @returns {Object}
   */
  _getErrorList: function(actionName) {
    var action = this._getAction(actionName);
    var data = this.getData(action.name);

    var errorList = {};

    _.each(_.keys(action.fields).reverse(), function(fieldName) {
      var required = action.fields[fieldName];
      if (required) {
        var field = this.getField(fieldName);
        if (field.isEmpty(data[fieldName])) {
          var label;
          var $textInput = field.$('input, textarea');
          var $labels = $('label[for="' + field.getAutoId() + '-input"]');
          if ($labels.length) {
            label = $labels.first().text();
          } else if ($textInput.attr('placeholder')) {
            label = $textInput.attr('placeholder');
          }
          if (label) {
            errorList[fieldName] = cm.language.get('{$label} is required.', {label: label});
          } else {
            errorList[fieldName] = cm.language.get('Required');
          }
        }
      }
    }, this);

    return errorList;
  }
});
<commit_msg>Introduce Form::hasField()<commit_after>/**
 * @class CM_Form_Abstract
 * @extends CM_View_Abstract
 */
var CM_Form_Abstract = CM_View_Abstract.extend({
  _class: 'CM_Form_Abstract',

  /** @type Object **/
  _fields: {},

  /** @type Object **/
  _actions: {},

  /** @type Boolean **/
  _stopErrorPropagation: false,

  initialize: function() {
    CM_View_Abstract.prototype.initialize.call(this);

    this._fields = {};
    this._actions = {};
  },

  events: {
    'reset': function() {
      _.each(this._fields, function(field) {
        field.reset();
      });
      this.resetErrors();
    }
  },

  ready: function() {
  },

  _ready: function() {
    var handler = this;

    _.each(this._actions, function(action, name) {
      var $btn = $('#' + this.getAutoId() + '-' + name + '-button');
      var event = $btn.data('event');
      if (!event) {
        event = 'click';
      }
      $btn.on(event, {action: name}, function(event) {
        event.preventDefault();
        event.stopPropagation();
        return handler.submit(event.data.action);
      });
    }, this);

    this.$el.on('submit', function() {
      handler.$el.find('input[type="submit"], button[type="submit"]').first().click();
      return false;
    });

    CM_View_Abstract.prototype._ready.call(this);
  },

  /**
   * @param {CM_FormField_Abstract} field
   */
  registerField: function(field) {
    this._fields[field.getName()] = field;

    field.on('change', function() {
      this.trigger('change');
    }, this);
  },

  /**
   * @param {String} name
   * @param {Object} presentation
   */
  registerAction: function(name, presentation) {
    this._actions[name] = presentation;
  },

  /**
   * @return CM_FormField_Abstract
   */
  getField: function(name) {
    if (!this._fields[name]) {
      cm.error.triggerThrow(this.getClass() + ' cannot find form field `' + name + '`');
    }
    return this._fields[name];
  },

  /**
   * @return Boolean
   */
  hasField: function(name) {
    return !!this._fields[name];
  },

  /**
   * @return jQuery
   */
  $: function(selector) {
    if (!selector) {
      return this.$el;
    }
    selector = selector.replace('#', '#' + this.getAutoId() + '-');
    return $(selector, this.el);
  },

  /**
   * @param {String|Null} [actionName]
   */
  getData: function(actionName) {
    var form_data = this.$().serializeArray();
    var action = actionName ? this._actions[actionName] : null;

    var data = {};
    var regex = /^([\w\-]+)(\[([^\]]+)?\])?$/;
    var name, match;

    for (var i = 0, item; item = form_data[i]; i++) {
      match = regex.exec(item.name);
      name = match[1];
      item.value = item.value || '';

      if (action && typeof action.fields[name] == 'undefined') {
        continue;
      }

      if (!match[2]) {
        // Scalar
        data[name] = item.value;
      } else if (match[2] == '[]') {
        // Array
        if (typeof data[name] == 'undefined') {
          data[name] = [];
        }
        data[name].push(item.value);
      } else if (match[3]) {
        // Associative array
        if (typeof data[name] == 'undefined') {
          data[name] = {};
        }
        data[name][match[3]] = item.value;
      }
    }

    return data;
  },

  /**
   * @param {String} [actionName]
   * @param {Object} [options]
   * @return Promise
   */
  submit: function(actionName, options) {
    options = _.defaults(options || {}, {
      handleErrors: true,
      disableUI: true
    });
    var action = this._getAction(actionName);
    var data = this.getData(action.name);
    var deferred = $.Deferred();

    var errorList = this._getErrorList(action.name);

    if (options.handleErrors) {
      _.each(this._fields, function(field, fieldName) {
        if (errorList[fieldName]) {
          field.error(errorList[fieldName]);
        } else {
          field.error(null);
        }
      }, this);
    }

    if (_.size(errorList)) {
      deferred.reject();

    } else {
      if (options.disableUI) {
        this.disable();
      }
      this.trigger('submit', [data]);

      var handler = this;
      cm.ajax('form', {viewInfoList: this.getViewInfoList(), actionName: action.name, data: data}, {
        success: function(response) {
          if (response.errors) {
            if (options.handleErrors) {
              for (var i = response.errors.length - 1, error; error = response.errors[i]; i--) {
                if (_.isArray(error)) {
                  handler.getField(error[1]).error(error[0]);
                } else {
                  handler.error(error);
                }
              }
            }

            handler.trigger('error error.' + action.name);
            deferred.reject();
          }

          if (response.exec) {
            handler.evaluation = new Function(response.exec);
            handler.evaluation();
          }

          if (response.messages) {
            for (var i = 0, msg; msg = response.messages[i]; i++) {
              handler.message(msg);
            }
          }

          if (!response.errors) {
            handler.trigger('success success.' + action.name, response.data);
            deferred.resolve(response.data);
          }
        },
        error: function(msg, type, isPublic) {
          handler._stopErrorPropagation = false;
          handler.trigger('error error.' + action.name, msg, type, isPublic);
          deferred.reject();
          return !handler._stopErrorPropagation;
        },
        complete: function() {
          if (options.disableUI) {
            handler.enable();
          }
          handler.trigger('complete');
        }
      });
    }

    return deferred.promise();
  },

  stopErrorPropagation: function() {
    this._stopErrorPropagation = true;
  },

  reset: function() {
    this.el.reset();
  },

  disable: function() {
    this.$().disable();
  },

  enable: function() {
    this.$().enable();
  },

  /**
   * @param {String} message
   */
  error: function(message) {
    cm.window.hint(message);
  },

  /**
   * @param {String} message
   */
  message: function(message) {
    cm.window.hint(message);
  },

  resetErrors: function() {
    _.each(this._fields, function(field) {
      field.error(null);
    });
  },

  /**
   * @param {String} [actionName]
   * @returns {Object}
   */
  _getAction: function(actionName) {
    actionName = actionName || _.first(_.keys(this._actions));
    var action = this._actions[actionName];
    if (!action) {
      cm.error.triggerThrow('Form `' + this.getClass() + '` has no action `' + actionName + '`.');
    }
    action.name = actionName;
    return action;
  },

  /**
   * @param {String} [actionName]
   * @returns {Object}
   */
  _getErrorList: function(actionName) {
    var action = this._getAction(actionName);
    var data = this.getData(action.name);

    var errorList = {};

    _.each(_.keys(action.fields).reverse(), function(fieldName) {
      var required = action.fields[fieldName];
      if (required) {
        var field = this.getField(fieldName);
        if (field.isEmpty(data[fieldName])) {
          var label;
          var $textInput = field.$('input, textarea');
          var $labels = $('label[for="' + field.getAutoId() + '-input"]');
          if ($labels.length) {
            label = $labels.first().text();
          } else if ($textInput.attr('placeholder')) {
            label = $textInput.attr('placeholder');
          }
          if (label) {
            errorList[fieldName] = cm.language.get('{$label} is required.', {label: label});
          } else {
            errorList[fieldName] = cm.language.get('Required');
          }
        }
      }
    }, this);

    return errorList;
  }
});
<|endoftext|>


<commit_before>/**
 * @class CM_View_Abstract
 * @extends Backbone.View
 */
var CM_View_Abstract = Backbone.View.extend({
	_class: 'CM_View_Abstract',

	_children: [],

	initialize: function() {
		this._children = [];

		if (this.getParent()) {
			this.getParent().registerChild(this);
		}
		this.events = this.collectEvents();

		if (this.actions) {
			this._bindActions(this.actions);
		}
		if (this.streams) {
			this._bindStreams(this.streams);
		}
		if (this.childrenEvents) {
			this._bindChildrenEvents(this.childrenEvents);
		}
		this.on('all', function(eventName, data) {
			cm.viewEvents.trigger(this, eventName, data);
		});
	},

	collectEvents: function() {
		var eventsObjects = [], currentConstructor = this.constructor, currentProto = currentConstructor.prototype;

		do {
			if (currentProto.hasOwnProperty('events')) {
				eventsObjects.unshift(currentProto.events);
			}
		} while (currentConstructor = ( currentProto = currentConstructor.__super__ ) && currentProto.constructor);
		eventsObjects.unshift({});
		return _.extend.apply(_, eventsObjects);
	},

	ready: function() {
	},

	_ready: function() {
		this.ready();
		_.each(this.getChildren(), function(child) {
			child._ready();
		});
	},

	/**
	 * @param {CM_View_Abstract} child
	 */
	registerChild: function(child) {
		this._children.push(child);
		child.options.parent = this;
	},

	/**
	 * @return CM_View_Abstract[]
	 */
	getChildren: function() {
		return this._children;
	},

	/**
	 * @param {String} className
	 * @return CM_View_Abstract|null
	 */
	findChild: function(className) {
		return _.find(this.getChildren(), function(child) {
			return _.contains(child.getClasses(), className);
		}) || null;
	},

	/**
	 * @return CM_View_Abstract|null
	 */
	getParent: function() {
		if (this.options.parent) {
			return this.options.parent;
		}
		return null;
	},

	/**
	 * @param {String} className
	 * @return CM_View_Abstract|null
	 */
	findParent: function(className) {
		var parent = this.getParent();
		if (!parent) {
			return null;
		}
		if (_.contains(parent.getClasses(), className)) {
			return parent;
		}
		return parent.findParent(className);
	},

	/**
	 * @return String
	 */
	getAutoId: function() {
		return this.el.id;
	},

	/**
	 * @return Object
	 */
	getParams: function() {
		return this.options.params || {};
	},

	/**
	 * @return string[]
	 */
	getClasses: function() {
		var classes = [this.getClass()];
		if ('CM_View_Abstract' != this.getClass()) {
			classes = classes.concat(this.constructor.__super__.getClasses());
		}
		return classes;
	},

	/**
	 * @return String
	 */
	getClass: function() {
		return this._class;
	},

	/**
	 * @param {Boolean} skipDomRemoval OPTIONAL
	 */
	remove: function(skipDomRemoval) {
		this.trigger("destruct");

		if (this.getParent()) {
			var siblings = this.getParent().getChildren();
			for (var i = 0, sibling; sibling = siblings[i]; i++) {
				if (sibling.getAutoId() == this.getAutoId()) {
					siblings.splice(i, 1);
				}
			}
		}

		_.each(_.clone(this.getChildren()), function(child) {
			child.remove();
		});

		delete cm.views[this.getAutoId()];

		if (!skipDomRemoval) {
			this.$().remove();
		}
	},

	/**
	 * @param {CM_View_Abstract} view
	 */
	replaceWith: function(view) {
		view._callbacks = this._callbacks;
		this.getParent().registerChild(view);
		this.$().replaceWith(view.$());
		this.remove(true);
	},

	disable: function() {
		this.$().disable();
	},

	enable: function() {
		this.$().enable();
	},

	/**
	 * @param {String} functionName
	 * @param {Object|Null} [params]
	 * @param {Object|Null} [callbacks]
	 * @return jqXHR
	 */
	ajax: function(functionName, params, callbacks) {
		callbacks = callbacks || {};
		params = params || {};
		var handler = this;
		var xhr = cm.ajax('ajax', {view: this._getArray(), method: functionName, params: params}, {
			success: function(response) {
				if (response.exec) {
					new Function(response.exec).call(handler);
				}
				if (callbacks.success) {
					return callbacks.success.call(handler, response.data);
				}
			},
			error: function(msg, type, isPublic) {
				if (callbacks.error) {
					return callbacks.error.call(handler, msg, type, isPublic);
				}
			},
			complete: function() {
				if (callbacks.complete) {
					return callbacks.complete.call(handler);
				}
			}
		});
		this.on('destruct', function() {
			xhr.abort();
		});
		return xhr;
	},

	/**
	 * @param {String} functionName
	 * @param {Object|Null} [params]
	 * @param {Object|Null} [callbacks]
	 * @return jqXHR
	 */
	ajaxModal: function(functionName, params, callbacks) {
		callbacks = callbacks || {};
		var handler = this;
		var callbackComplete = callbacks.complete;
		callbacks.complete = function() {
			handler.enable();
			if (callbackComplete) {
				return callbackComplete(handler);
			}
		};
		this.disable();
		return this.ajax(functionName, params, callbacks);
	},

	/**
	 * @param {String} className
	 * @param {Object|Null} [params]
	 * @param {Object|Null} [options]
	 * @return jqXHR
	 */
	loadComponent: function(className, params, options) {
		options = options || {};
		params = params || {};
		params.className = className;
		var success = options.success ? options.success : this.popOut;
		options.success = function(response) {
			this._injectView(response, success);
		};
		return this.ajaxModal('loadComponent', params, options);
	},

	/**
	 * @param {String} path
	 * @param {Object} [options]
	 * @return jqXHR
	 */
	loadPage: function(path, options) {
		options = options || {};
		var success = options.success;
		options.success = function(response) {
			this._injectView(response, success);
		};
		return this.ajaxModal('loadPage', {path: path}, options);
	},

	/**
	 * @param {Object} response
	 * @param {Function} [successPre]
	 * @private
	 */
	_injectView: function(response, successPre) {
		cm.window.appendHidden(response.html);
		new Function(response.js).call(this);
		var view = cm.views[response.autoId];
		this.registerChild(view);
		if (successPre) {
			successPre.call(view, response);
		}
		view._ready();
	},

	/**
	 * @param {int} actionVerb
	 * @param {int} modelType
	 * @param {Function} callback fn(CM_Action_Abstract action, CM_Model_Abstract model, array data)
	 */
	bindAction: function(actionVerb, modelType, callback) {
		cm.action.bind(actionVerb, modelType, callback, this);
		this.on('destruct', function() {
			cm.action.unbind(actionVerb, modelType, callback, this);
		});
	},

	/**
	 * @param {String} event
	 * @param {Function} callback fn(array data)
	 */
	bindStream: function(event, callback) {
		var namespace = this.getClass() + ':' + event;
		cm.stream.bind(namespace, callback, this);
		this.on('destruct', function() {
			cm.stream.unbind(namespace, callback, this);
		});
	},

	/**
	 * @param {String} viewClassName
	 * @param {String} event
	 * @param {Function} callback fn(CM_View_Abstract view, array data)
	 */
	bindChildrenEvent: function(viewClassName, event, callback) {
		cm.viewEvents.bind(this, viewClassName, event, callback, this);
		this.on('destruct', function() {
			cm.viewEvents.unbind(this, viewClassName, event, callback, this);
		});
	},

	/**
	 * @param {String|Function} callback
	 * @param {int} interval
	 * @return {int}
	 */
	setInterval: function(callback, interval) {
		var id = window.setInterval(callback, interval);
		this.on('destruct', function() {
			window.clearInterval(id);
		});
		return id;
	},

	/**
	 * @param {String|Function} callback
	 * @param {int} timeout
	 * @return {int}
	 */
	setTimeout: function(callback, timeout) {
		var id = window.setTimeout(callback, timeout);
		this.on('destruct', function() {
			window.clearTimeout(id);
		});
		return id;
	},

	/**
	 * @param {Object} actions
	 */
	_bindActions: function(actions) {
		_.each(actions, function(callback, key) {
			var match = key.match(/^(\S+)\s+(.+)$/);
			var modelType = cm.model.types[match[1]];
			var actionNames = match[2].split(/\s*,\s*/);
			_.each(actionNames, function(actionName) {
				var actionVerb = cm.action.verbs[actionName];
				this.bindAction(actionVerb, modelType, callback);
			}, this);
		}, this);
	},

	/**
	 * @param {Object} streams
	 */
	_bindStreams: function(streams) {
		_.each(streams, function(callback, key) {
			this.bindStream(key, callback);
		}, this);
	},

	/**
	 * @param {Object} events
	 */
	_bindChildrenEvents: function(events) {
		_.each(events, function(callback, key) {
			var match = key.match(/^(\S+)\s+(.+)$/);
			var viewName = match[1];
			var eventNames = match[2].split(/\s*,\s*/);
			_.each(eventNames, function(eventName) {
				this.bindChildrenEvent(viewName, eventName, callback);
			}, this);
		}, this);
	},

	/**
	 * @return Object
	 */
	_getArray: function() {
		return {
			className: this.getClass(),
			id: this.getAutoId(),
			params: this.getParams(),
			parentId: this.getParent() ? this.getParent().getAutoId() : null
		};
	}
});<commit_msg>Fix: loadComponent success popOut reference doesnt work<commit_after>/**
 * @class CM_View_Abstract
 * @extends Backbone.View
 */
var CM_View_Abstract = Backbone.View.extend({
	_class: 'CM_View_Abstract',

	_children: [],

	initialize: function() {
		this._children = [];

		if (this.getParent()) {
			this.getParent().registerChild(this);
		}
		this.events = this.collectEvents();

		if (this.actions) {
			this._bindActions(this.actions);
		}
		if (this.streams) {
			this._bindStreams(this.streams);
		}
		if (this.childrenEvents) {
			this._bindChildrenEvents(this.childrenEvents);
		}
		this.on('all', function(eventName, data) {
			cm.viewEvents.trigger(this, eventName, data);
		});
	},

	collectEvents: function() {
		var eventsObjects = [], currentConstructor = this.constructor, currentProto = currentConstructor.prototype;

		do {
			if (currentProto.hasOwnProperty('events')) {
				eventsObjects.unshift(currentProto.events);
			}
		} while (currentConstructor = ( currentProto = currentConstructor.__super__ ) && currentProto.constructor);
		eventsObjects.unshift({});
		return _.extend.apply(_, eventsObjects);
	},

	ready: function() {
	},

	_ready: function() {
		this.ready();
		_.each(this.getChildren(), function(child) {
			child._ready();
		});
	},

	/**
	 * @param {CM_View_Abstract} child
	 */
	registerChild: function(child) {
		this._children.push(child);
		child.options.parent = this;
	},

	/**
	 * @return CM_View_Abstract[]
	 */
	getChildren: function() {
		return this._children;
	},

	/**
	 * @param {String} className
	 * @return CM_View_Abstract|null
	 */
	findChild: function(className) {
		return _.find(this.getChildren(), function(child) {
			return _.contains(child.getClasses(), className);
		}) || null;
	},

	/**
	 * @return CM_View_Abstract|null
	 */
	getParent: function() {
		if (this.options.parent) {
			return this.options.parent;
		}
		return null;
	},

	/**
	 * @param {String} className
	 * @return CM_View_Abstract|null
	 */
	findParent: function(className) {
		var parent = this.getParent();
		if (!parent) {
			return null;
		}
		if (_.contains(parent.getClasses(), className)) {
			return parent;
		}
		return parent.findParent(className);
	},

	/**
	 * @return String
	 */
	getAutoId: function() {
		return this.el.id;
	},

	/**
	 * @return Object
	 */
	getParams: function() {
		return this.options.params || {};
	},

	/**
	 * @return string[]
	 */
	getClasses: function() {
		var classes = [this.getClass()];
		if ('CM_View_Abstract' != this.getClass()) {
			classes = classes.concat(this.constructor.__super__.getClasses());
		}
		return classes;
	},

	/**
	 * @return String
	 */
	getClass: function() {
		return this._class;
	},

	/**
	 * @param {Boolean} skipDomRemoval OPTIONAL
	 */
	remove: function(skipDomRemoval) {
		this.trigger("destruct");

		if (this.getParent()) {
			var siblings = this.getParent().getChildren();
			for (var i = 0, sibling; sibling = siblings[i]; i++) {
				if (sibling.getAutoId() == this.getAutoId()) {
					siblings.splice(i, 1);
				}
			}
		}

		_.each(_.clone(this.getChildren()), function(child) {
			child.remove();
		});

		delete cm.views[this.getAutoId()];

		if (!skipDomRemoval) {
			this.$().remove();
		}
	},

	/**
	 * @param {CM_View_Abstract} view
	 */
	replaceWith: function(view) {
		view._callbacks = this._callbacks;
		this.getParent().registerChild(view);
		this.$().replaceWith(view.$());
		this.remove(true);
	},

	disable: function() {
		this.$().disable();
	},

	enable: function() {
		this.$().enable();
	},

	/**
	 * @param {String} functionName
	 * @param {Object|Null} [params]
	 * @param {Object|Null} [callbacks]
	 * @return jqXHR
	 */
	ajax: function(functionName, params, callbacks) {
		callbacks = callbacks || {};
		params = params || {};
		var handler = this;
		var xhr = cm.ajax('ajax', {view: this._getArray(), method: functionName, params: params}, {
			success: function(response) {
				if (response.exec) {
					new Function(response.exec).call(handler);
				}
				if (callbacks.success) {
					return callbacks.success.call(handler, response.data);
				}
			},
			error: function(msg, type, isPublic) {
				if (callbacks.error) {
					return callbacks.error.call(handler, msg, type, isPublic);
				}
			},
			complete: function() {
				if (callbacks.complete) {
					return callbacks.complete.call(handler);
				}
			}
		});
		this.on('destruct', function() {
			xhr.abort();
		});
		return xhr;
	},

	/**
	 * @param {String} functionName
	 * @param {Object|Null} [params]
	 * @param {Object|Null} [callbacks]
	 * @return jqXHR
	 */
	ajaxModal: function(functionName, params, callbacks) {
		callbacks = callbacks || {};
		var handler = this;
		var callbackComplete = callbacks.complete;
		callbacks.complete = function() {
			handler.enable();
			if (callbackComplete) {
				return callbackComplete(handler);
			}
		};
		this.disable();
		return this.ajax(functionName, params, callbacks);
	},

	/**
	 * @param {String} className
	 * @param {Object|Null} [params]
	 * @param {Object|Null} [options]
	 * @return jqXHR
	 */
	loadComponent: function(className, params, options) {
		options = options || {};
		params = params || {};
		params.className = className;
		var success = options.success ? options.success : function() {
			this.popOut();
		};
		options.success = function(response) {
			this._injectView(response, success);
		};
		return this.ajaxModal('loadComponent', params, options);
	},

	/**
	 * @param {String} path
	 * @param {Object} [options]
	 * @return jqXHR
	 */
	loadPage: function(path, options) {
		options = options || {};
		var success = options.success;
		options.success = function(response) {
			this._injectView(response, success);
		};
		return this.ajaxModal('loadPage', {path: path}, options);
	},

	/**
	 * @param {Object} response
	 * @param {Function} [successPre]
	 * @private
	 */
	_injectView: function(response, successPre) {
		cm.window.appendHidden(response.html);
		new Function(response.js).call(this);
		var view = cm.views[response.autoId];
		this.registerChild(view);
		if (successPre) {
			successPre.call(view, response);
		}
		view._ready();
	},

	/**
	 * @param {int} actionVerb
	 * @param {int} modelType
	 * @param {Function} callback fn(CM_Action_Abstract action, CM_Model_Abstract model, array data)
	 */
	bindAction: function(actionVerb, modelType, callback) {
		cm.action.bind(actionVerb, modelType, callback, this);
		this.on('destruct', function() {
			cm.action.unbind(actionVerb, modelType, callback, this);
		});
	},

	/**
	 * @param {String} event
	 * @param {Function} callback fn(array data)
	 */
	bindStream: function(event, callback) {
		var namespace = this.getClass() + ':' + event;
		cm.stream.bind(namespace, callback, this);
		this.on('destruct', function() {
			cm.stream.unbind(namespace, callback, this);
		});
	},

	/**
	 * @param {String} viewClassName
	 * @param {String} event
	 * @param {Function} callback fn(CM_View_Abstract view, array data)
	 */
	bindChildrenEvent: function(viewClassName, event, callback) {
		cm.viewEvents.bind(this, viewClassName, event, callback, this);
		this.on('destruct', function() {
			cm.viewEvents.unbind(this, viewClassName, event, callback, this);
		});
	},

	/**
	 * @param {String|Function} callback
	 * @param {int} interval
	 * @return {int}
	 */
	setInterval: function(callback, interval) {
		var id = window.setInterval(callback, interval);
		this.on('destruct', function() {
			window.clearInterval(id);
		});
		return id;
	},

	/**
	 * @param {String|Function} callback
	 * @param {int} timeout
	 * @return {int}
	 */
	setTimeout: function(callback, timeout) {
		var id = window.setTimeout(callback, timeout);
		this.on('destruct', function() {
			window.clearTimeout(id);
		});
		return id;
	},

	/**
	 * @param {Object} actions
	 */
	_bindActions: function(actions) {
		_.each(actions, function(callback, key) {
			var match = key.match(/^(\S+)\s+(.+)$/);
			var modelType = cm.model.types[match[1]];
			var actionNames = match[2].split(/\s*,\s*/);
			_.each(actionNames, function(actionName) {
				var actionVerb = cm.action.verbs[actionName];
				this.bindAction(actionVerb, modelType, callback);
			}, this);
		}, this);
	},

	/**
	 * @param {Object} streams
	 */
	_bindStreams: function(streams) {
		_.each(streams, function(callback, key) {
			this.bindStream(key, callback);
		}, this);
	},

	/**
	 * @param {Object} events
	 */
	_bindChildrenEvents: function(events) {
		_.each(events, function(callback, key) {
			var match = key.match(/^(\S+)\s+(.+)$/);
			var viewName = match[1];
			var eventNames = match[2].split(/\s*,\s*/);
			_.each(eventNames, function(eventName) {
				this.bindChildrenEvent(viewName, eventName, callback);
			}, this);
		}, this);
	},

	/**
	 * @return Object
	 */
	_getArray: function() {
		return {
			className: this.getClass(),
			id: this.getAutoId(),
			params: this.getParams(),
			parentId: this.getParent() ? this.getParent().getAutoId() : null
		};
	}
});<|endoftext|>


<commit_before>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('options', function() {
  const OptionsPage = options.OptionsPage;

  /**
   * Encapsulated handling of a search bubble.
   * @constructor
   */
  function SearchBubble(text) {
    var el = cr.doc.createElement('div');
    SearchBubble.decorate(el);
    el.textContent = text;
    return el;
  }

  SearchBubble.decorate = function(el) {
    el.__proto__ = SearchBubble.prototype;
    el.decorate();
  };

  SearchBubble.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      this.className = 'search-bubble';

      // We create a timer to periodically update the position of the bubbles.
      // While this isn't all that desirable, it's the only sure-fire way of
      // making sure the bubbles stay in the correct location as sections
      // may dynamically change size at any time.
      var self = this;
      this.intervalId = setInterval(this.updatePosition.bind(this), 250);
    },

    /**
     * Clear the interval timer and remove the element from the page.
     */
    dispose: function() {
      clearInterval(this.intervalId);

      var parent = this.parentNode;
      if (parent)
        parent.removeChild(this);
    },

    /**
     * Update the position of the bubble.  Called at creation time and then
     * periodically while the bubble remains visible.
     */
    updatePosition: function() {
      // This bubble is 'owned' by the next sibling.
      var owner = this.nextSibling;

      // If there isn't an offset parent, we have nothing to do.
      if (!owner.offsetParent)
        return;

      // Position the bubble below the location of the owner.
      var left = owner.offsetLeft + owner.offsetWidth / 2 -
          this.offsetWidth / 2;
      var top = owner.offsetTop + owner.offsetHeight;

      // Update the position in the CSS.  Cache the last values for
      // best performance.
      if (left != this.lastLeft) {
        this.style.left = left + 'px';
        this.lastLeft = left;
      }
      if (top != this.lastTop) {
        this.style.top = top + 'px';
        this.lastTop = top;
      }
    }
  }

  /**
   * Encapsulated handling of the search page.
   * @constructor
   */
  function SearchPage() {
    OptionsPage.call(this, 'search', templateData.searchPageTabTitle,
        'searchPage');
    this.searchActive = false;
  }

  cr.addSingletonGetter(SearchPage);

  SearchPage.prototype = {
    // Inherit SearchPage from OptionsPage.
    __proto__: OptionsPage.prototype,

    /**
     * Initialize the page.
     */
    initializePage: function() {
      // Call base class implementation to start preference initialization.
      OptionsPage.prototype.initializePage.call(this);

      var self = this;

      // Create a search field element.
      var searchField = document.createElement('input');
      searchField.id = 'search-field';
      searchField.type = 'search';
      searchField.incremental = true;
      searchField.placeholder = localStrings.getString('searchPlaceholder');
      this.searchField = searchField;

      // Replace the contents of the navigation tab with the search field.
      self.tab.textContent = '';
      self.tab.appendChild(searchField);
      self.tab.onclick = self.tab.onkeypress = undefined;

      // Handle search events. (No need to throttle, WebKit's search field
      // will do that automatically.)
      searchField.onsearch = function(e) {
        self.setSearchText_(SearchPage.canonicalizeQuery(this.value));
      };

      // We update the history stack every time the search field blurs. This way
      // we get a history entry for each search, roughly, but not each letter
      // typed.
      searchField.onblur = function(e) {
        var query = SearchPage.canonicalizeQuery(searchField.value);
        if (!query)
          return;

        // Don't push the same page onto the history stack more than once (if
        // the user clicks in the search field and away several times).
        var currentHash = location.hash;
        var newHash = '#' + escape(query);
        if (currentHash == newHash)
          return;

        // If there is no hash on the current URL, the history entry has no
        // search query. Replace the history entry with no search with an entry
        // that does have a search. Otherwise, add it onto the history stack.
        var historyFunction = currentHash ? window.history.pushState :
                                            window.history.replaceState;
        historyFunction.call(
            window.history,
            {pageName: self.name},
            self.title,
            '/' + self.name + newHash);
      };

      // Install handler for key presses.
      document.addEventListener('keydown',
                                this.keyDownEventHandler_.bind(this));

      // Focus the search field by default.
      searchField.focus();
    },

    /**
     * @inheritDoc
     */
    get sticky() {
      return true;
    },

    /**
     * Called after this page has shown.
     */
    didShowPage: function() {
      // This method is called by the Options page after all pages have
      // had their visibilty attribute set.  At this point we can perform the
      // search specific DOM manipulation.
      this.setSearchActive_(true);
    },

    /**
     * Called before this page will be hidden.
     */
    willHidePage: function() {
      // This method is called by the Options page before all pages have
      // their visibilty attribute set.  Before that happens, we need to
      // undo the search specific DOM manipulation that was performed in
      // didShowPage.
      this.setSearchActive_(false);
    },

    /**
     * Update the UI to reflect whether we are in a search state.
     * @param {boolean} active True if we are on the search page.
     * @private
     */
    setSearchActive_: function(active) {
      // It's fine to exit if search wasn't active and we're not going to
      // activate it now.
      if (!this.searchActive_ && !active)
        return;

      this.searchActive_ = active;

      if (active) {
        var hash = location.hash;
        if (hash)
          this.searchField.value = unescape(hash.slice(1));
      } else {
          // Just wipe out any active search text since it's no longer relevant.
        this.searchField.value = '';
      }

      var pagesToSearch = this.getSearchablePages_();
      for (var key in pagesToSearch) {
        var page = pagesToSearch[key];

        if (!active)
          page.visible = false;

        // Update the visible state of all top-level elements that are not
        // sections (ie titles, button strips).  We do this before changing
        // the page visibility to avoid excessive re-draw.
        for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) {
          if (active) {
            if (childDiv.tagName != 'SECTION')
              childDiv.classList.add('search-hidden');
          } else {
            childDiv.classList.remove('search-hidden');
          }
        }

        if (active) {
          // When search is active, remove the 'hidden' tag.  This tag may have
          // been added by the OptionsPage.
          page.pageDiv.classList.remove('hidden');
        }
      }

      if (active) {
        this.setSearchText_(this.searchField.value);
      } else {
        // After hiding all page content, remove any search results.
        this.unhighlightMatches_();
        this.removeSearchBubbles_();
      }
    },

    /**
     * Set the current search criteria.
     * @param {string} text Search text.
     * @private
     */
    setSearchText_: function(text) {
      // Toggle the search page if necessary.
      if (text.length) {
        if (!this.searchActive_)
          OptionsPage.navigateToPage(this.name);
      } else {
        if (this.searchActive_)
          OptionsPage.showDefaultPage();
        return;
      }

      var foundMatches = false;
      var bubbleControls = [];

      // Remove any prior search results.
      this.unhighlightMatches_();
      this.removeSearchBubbles_();

      // Generate search text by applying lowercase and escaping any characters
      // that would be problematic for regular expressions.
      var searchText =
          text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');

      // Generate a regular expression and replace string for hilighting
      // search terms.
      var regEx = new RegExp('(' + searchText + ')', 'ig');
      var replaceString = '$1';

      // Initialize all sections.  If the search string matches a title page,
      // show sections for that page.
      var page, pageMatch, childDiv, length;
      var pagesToSearch = this.getSearchablePages_();
      for (var key in pagesToSearch) {
        page = pagesToSearch[key];
        pageMatch = false;
        if (searchText.length) {
          pageMatch = this.performReplace_(regEx, replaceString, page.tab);
        }
        if (pageMatch)
          foundMatches = true;
        for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) {
          if (childDiv.tagName == 'SECTION') {
            if (pageMatch) {
              childDiv.classList.remove('search-hidden');
            } else {
              childDiv.classList.add('search-hidden');
            }
          }
        }
      }

      if (searchText.length) {
        // Search all top-level sections for anchored string matches.
        for (var key in pagesToSearch) {
          page = pagesToSearch[key];
          for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) {
            if (childDiv.tagName == 'SECTION' &&
                this.performReplace_(regEx, replaceString, childDiv)) {
              childDiv.classList.remove('search-hidden');
              foundMatches = true;
            }
          }
        }

        // Search all sub-pages, generating an array of top-level sections that
        // we need to make visible.
        var subPagesToSearch = this.getSearchableSubPages_();
        var control, node;
        for (var key in subPagesToSearch) {
          page = subPagesToSearch[key];
          if (this.performReplace_(regEx, replaceString, page.pageDiv)) {
            // Reveal the section for this search result.
            section = page.associatedSection;
            if (section)
              section.classList.remove('search-hidden');

            // Identify any controls that should have bubbles.
            var controls = page.associatedControls;
            if (controls) {
              length = controls.length;
              for (var i = 0; i < length; i++)
                bubbleControls.push(controls[i]);
            }

            foundMatches = true;
          }
        }
      }

      // Configure elements on the search results page based on search results.
      if (foundMatches)
        $('searchPageNoMatches').classList.add('search-hidden');
      else
        $('searchPageNoMatches').classList.remove('search-hidden');

      // Create search balloons for sub-page results.
      length = bubbleControls.length;
      for (var i = 0; i < length; i++)
        this.createSearchBubble_(bubbleControls[i], text);
    },

    /**
     * Performs a string replacement based on a regex and replace string.
     * @param {RegEx} regex A regular expression for finding search matches.
     * @param {String} replace A string to apply the replace operation.
     * @param {Element} element An HTML container element.
     * @returns {Boolean} true if the element was changed.
     * @private
     */
    performReplace_: function(regex, replace, element) {
      var found = false;
      var div, child, tmp;

      // Walk the tree, searching each TEXT node.
      var walker = document.createTreeWalker(element,
                                             NodeFilter.SHOW_TEXT,
                                             null,
                                             false);
      var node = walker.nextNode();
      while (node) {
        // Perform a search and replace on the text node value.
        var newValue = node.nodeValue.replace(regex, replace);
        if (newValue != node.nodeValue) {
          // The text node has changed so that means we found at least one
          // match.
          found = true;

          // Create a temporary div element and set the innerHTML to the new
          // value.
          div = document.createElement('div');
          div.innerHTML = newValue;

          // Insert all the child nodes of the temporary div element into the
          // document, before the original node.
          child = div.firstChild;
          while (child = div.firstChild) {
            node.parentNode.insertBefore(child, node);
          };

          // Delete the old text node and advance the walker to the next
          // node.
          tmp = node;
          node = walker.nextNode();
          tmp.parentNode.removeChild(tmp);
        } else {
          node = walker.nextNode();
        }
      }

      return found;
    },

    /**
     * Removes all search highlight tags from the document.
     * @private
     */
    unhighlightMatches_: function() {
      // Find all search highlight elements.
      var elements = document.querySelectorAll('.search-highlighted');

      // For each element, remove the highlighting.
      var parent, i;
      for (var i = 0, node; node = elements[i]; i++) {
        parent = node.parentNode;

        // Replace the highlight element with the first child (the text node).
        parent.replaceChild(node.firstChild, node);

        // Normalize the parent so that multiple text nodes will be combined.
        parent.normalize();
      }
    },

    /**
     * Creates a search result bubble attached to an element.
     * @param {Element} element An HTML element, usually a button.
     * @param {string} text A string to show in the bubble.
     * @private
     */
    createSearchBubble_: function(element, text) {
      // avoid appending multiple ballons to a button.
      var sibling = element.previousElementSibling;
      if (sibling && sibling.classList.contains('search-bubble'))
        return;

      var parent = element.parentElement;
      if (parent) {
        var bubble = new SearchBubble(text);
        parent.insertBefore(bubble, element);
        bubble.updatePosition();
      }
    },

    /**
     * Removes all search match bubbles.
     * @private
     */
    removeSearchBubbles_: function() {
      var elements = document.querySelectorAll('.search-bubble');
      var length = elements.length;
      for (var i = 0; i < length; i++)
        elements[i].dispose();
    },

    /**
     * Builds a list of top-level pages to search.  Omits the search page and
     * all sub-pages.
     * @returns {Array} An array of pages to search.
     * @private
     */
    getSearchablePages_: function() {
      var name, page, pages = [];
      for (name in OptionsPage.registeredPages) {
        if (name != this.name) {
          page = OptionsPage.registeredPages[name];
          if (!page.parentPage)
            pages.push(page);
        }
      }
      return pages;
    },

    /**
     * Builds a list of sub-pages (and overlay pages) to search.  Ignore pages
     * that have no associated controls.
     * @returns {Array} An array of pages to search.
     * @private
     */
    getSearchableSubPages_: function() {
      var name, pageInfo, page, pages = [];
      for (name in OptionsPage.registeredPages) {
        page = OptionsPage.registeredPages[name];
        if (page.parentPage && page.associatedSection)
          pages.push(page);
      }
      for (name in OptionsPage.registeredOverlayPages) {
        page = OptionsPage.registeredOverlayPages[name];
        if (page.associatedSection && page.pageDiv != undefined)
          pages.push(page);
      }
      return pages;
    },

    /**
     * A function to handle key press events.
     * @return {Event} a keydown event.
     * @private
     */
    keyDownEventHandler_: function(event) {
      // Focus the search field on an unused forward-slash.
      if (event.keyCode == 191 &&
          !/INPUT|SELECT|BUTTON|TEXTAREA/.test(event.target.tagName)) {
        this.searchField.focus();
        event.stopPropagation();
        event.preventDefault();
      }
    },
  };

  /**
   * Standardizes a user-entered text query by removing extra whitespace.
   * @param {string} The user-entered text.
   * @return {string} The trimmed query.
   */
  SearchPage.canonicalizeQuery = function(text) {
    // Trim beginning and ending whitespace.
    return text.replace(/^\s+|\s+$/g, '');
  };

  // Export
  return {
    SearchPage: SearchPage
  };

});
<commit_msg>web-ui settings: Allow escape key to cancel the current search.<commit_after>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('options', function() {
  const OptionsPage = options.OptionsPage;

  /**
   * Encapsulated handling of a search bubble.
   * @constructor
   */
  function SearchBubble(text) {
    var el = cr.doc.createElement('div');
    SearchBubble.decorate(el);
    el.textContent = text;
    return el;
  }

  SearchBubble.decorate = function(el) {
    el.__proto__ = SearchBubble.prototype;
    el.decorate();
  };

  SearchBubble.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      this.className = 'search-bubble';

      // We create a timer to periodically update the position of the bubbles.
      // While this isn't all that desirable, it's the only sure-fire way of
      // making sure the bubbles stay in the correct location as sections
      // may dynamically change size at any time.
      var self = this;
      this.intervalId = setInterval(this.updatePosition.bind(this), 250);
    },

    /**
     * Clear the interval timer and remove the element from the page.
     */
    dispose: function() {
      clearInterval(this.intervalId);

      var parent = this.parentNode;
      if (parent)
        parent.removeChild(this);
    },

    /**
     * Update the position of the bubble.  Called at creation time and then
     * periodically while the bubble remains visible.
     */
    updatePosition: function() {
      // This bubble is 'owned' by the next sibling.
      var owner = this.nextSibling;

      // If there isn't an offset parent, we have nothing to do.
      if (!owner.offsetParent)
        return;

      // Position the bubble below the location of the owner.
      var left = owner.offsetLeft + owner.offsetWidth / 2 -
          this.offsetWidth / 2;
      var top = owner.offsetTop + owner.offsetHeight;

      // Update the position in the CSS.  Cache the last values for
      // best performance.
      if (left != this.lastLeft) {
        this.style.left = left + 'px';
        this.lastLeft = left;
      }
      if (top != this.lastTop) {
        this.style.top = top + 'px';
        this.lastTop = top;
      }
    }
  }

  /**
   * Encapsulated handling of the search page.
   * @constructor
   */
  function SearchPage() {
    OptionsPage.call(this, 'search', templateData.searchPageTabTitle,
        'searchPage');
    this.searchActive = false;
  }

  cr.addSingletonGetter(SearchPage);

  SearchPage.prototype = {
    // Inherit SearchPage from OptionsPage.
    __proto__: OptionsPage.prototype,

    /**
     * Initialize the page.
     */
    initializePage: function() {
      // Call base class implementation to start preference initialization.
      OptionsPage.prototype.initializePage.call(this);

      var self = this;

      // Create a search field element.
      var searchField = document.createElement('input');
      searchField.id = 'search-field';
      searchField.type = 'search';
      searchField.incremental = true;
      searchField.placeholder = localStrings.getString('searchPlaceholder');
      this.searchField = searchField;

      // Replace the contents of the navigation tab with the search field.
      self.tab.textContent = '';
      self.tab.appendChild(searchField);
      self.tab.onclick = self.tab.onkeypress = undefined;

      // Handle search events. (No need to throttle, WebKit's search field
      // will do that automatically.)
      searchField.onsearch = function(e) {
        self.setSearchText_(SearchPage.canonicalizeQuery(this.value));
      };

      // We update the history stack every time the search field blurs. This way
      // we get a history entry for each search, roughly, but not each letter
      // typed.
      searchField.onblur = function(e) {
        var query = SearchPage.canonicalizeQuery(searchField.value);
        if (!query)
          return;

        // Don't push the same page onto the history stack more than once (if
        // the user clicks in the search field and away several times).
        var currentHash = location.hash;
        var newHash = '#' + escape(query);
        if (currentHash == newHash)
          return;

        // If there is no hash on the current URL, the history entry has no
        // search query. Replace the history entry with no search with an entry
        // that does have a search. Otherwise, add it onto the history stack.
        var historyFunction = currentHash ? window.history.pushState :
                                            window.history.replaceState;
        historyFunction.call(
            window.history,
            {pageName: self.name},
            self.title,
            '/' + self.name + newHash);
      };

      // Install handler for key presses.
      document.addEventListener('keydown',
                                this.keyDownEventHandler_.bind(this));

      // Focus the search field by default.
      searchField.focus();
    },

    /**
     * @inheritDoc
     */
    get sticky() {
      return true;
    },

    /**
     * Called after this page has shown.
     */
    didShowPage: function() {
      // This method is called by the Options page after all pages have
      // had their visibilty attribute set.  At this point we can perform the
      // search specific DOM manipulation.
      this.setSearchActive_(true);
    },

    /**
     * Called before this page will be hidden.
     */
    willHidePage: function() {
      // This method is called by the Options page before all pages have
      // their visibilty attribute set.  Before that happens, we need to
      // undo the search specific DOM manipulation that was performed in
      // didShowPage.
      this.setSearchActive_(false);
    },

    /**
     * Update the UI to reflect whether we are in a search state.
     * @param {boolean} active True if we are on the search page.
     * @private
     */
    setSearchActive_: function(active) {
      // It's fine to exit if search wasn't active and we're not going to
      // activate it now.
      if (!this.searchActive_ && !active)
        return;

      this.searchActive_ = active;

      if (active) {
        var hash = location.hash;
        if (hash)
          this.searchField.value = unescape(hash.slice(1));
      } else {
          // Just wipe out any active search text since it's no longer relevant.
        this.searchField.value = '';
      }

      var pagesToSearch = this.getSearchablePages_();
      for (var key in pagesToSearch) {
        var page = pagesToSearch[key];

        if (!active)
          page.visible = false;

        // Update the visible state of all top-level elements that are not
        // sections (ie titles, button strips).  We do this before changing
        // the page visibility to avoid excessive re-draw.
        for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) {
          if (active) {
            if (childDiv.tagName != 'SECTION')
              childDiv.classList.add('search-hidden');
          } else {
            childDiv.classList.remove('search-hidden');
          }
        }

        if (active) {
          // When search is active, remove the 'hidden' tag.  This tag may have
          // been added by the OptionsPage.
          page.pageDiv.classList.remove('hidden');
        }
      }

      if (active) {
        this.setSearchText_(this.searchField.value);
      } else {
        // After hiding all page content, remove any search results.
        this.unhighlightMatches_();
        this.removeSearchBubbles_();
      }
    },

    /**
     * Set the current search criteria.
     * @param {string} text Search text.
     * @private
     */
    setSearchText_: function(text) {
      // Toggle the search page if necessary.
      if (text.length) {
        if (!this.searchActive_)
          OptionsPage.navigateToPage(this.name);
      } else {
        if (this.searchActive_)
          OptionsPage.showDefaultPage();
        return;
      }

      var foundMatches = false;
      var bubbleControls = [];

      // Remove any prior search results.
      this.unhighlightMatches_();
      this.removeSearchBubbles_();

      // Generate search text by applying lowercase and escaping any characters
      // that would be problematic for regular expressions.
      var searchText =
          text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');

      // Generate a regular expression and replace string for hilighting
      // search terms.
      var regEx = new RegExp('(' + searchText + ')', 'ig');
      var replaceString = '$1';

      // Initialize all sections.  If the search string matches a title page,
      // show sections for that page.
      var page, pageMatch, childDiv, length;
      var pagesToSearch = this.getSearchablePages_();
      for (var key in pagesToSearch) {
        page = pagesToSearch[key];
        pageMatch = false;
        if (searchText.length) {
          pageMatch = this.performReplace_(regEx, replaceString, page.tab);
        }
        if (pageMatch)
          foundMatches = true;
        for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) {
          if (childDiv.tagName == 'SECTION') {
            if (pageMatch) {
              childDiv.classList.remove('search-hidden');
            } else {
              childDiv.classList.add('search-hidden');
            }
          }
        }
      }

      if (searchText.length) {
        // Search all top-level sections for anchored string matches.
        for (var key in pagesToSearch) {
          page = pagesToSearch[key];
          for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) {
            if (childDiv.tagName == 'SECTION' &&
                this.performReplace_(regEx, replaceString, childDiv)) {
              childDiv.classList.remove('search-hidden');
              foundMatches = true;
            }
          }
        }

        // Search all sub-pages, generating an array of top-level sections that
        // we need to make visible.
        var subPagesToSearch = this.getSearchableSubPages_();
        var control, node;
        for (var key in subPagesToSearch) {
          page = subPagesToSearch[key];
          if (this.performReplace_(regEx, replaceString, page.pageDiv)) {
            // Reveal the section for this search result.
            section = page.associatedSection;
            if (section)
              section.classList.remove('search-hidden');

            // Identify any controls that should have bubbles.
            var controls = page.associatedControls;
            if (controls) {
              length = controls.length;
              for (var i = 0; i < length; i++)
                bubbleControls.push(controls[i]);
            }

            foundMatches = true;
          }
        }
      }

      // Configure elements on the search results page based on search results.
      if (foundMatches)
        $('searchPageNoMatches').classList.add('search-hidden');
      else
        $('searchPageNoMatches').classList.remove('search-hidden');

      // Create search balloons for sub-page results.
      length = bubbleControls.length;
      for (var i = 0; i < length; i++)
        this.createSearchBubble_(bubbleControls[i], text);
    },

    /**
     * Performs a string replacement based on a regex and replace string.
     * @param {RegEx} regex A regular expression for finding search matches.
     * @param {String} replace A string to apply the replace operation.
     * @param {Element} element An HTML container element.
     * @returns {Boolean} true if the element was changed.
     * @private
     */
    performReplace_: function(regex, replace, element) {
      var found = false;
      var div, child, tmp;

      // Walk the tree, searching each TEXT node.
      var walker = document.createTreeWalker(element,
                                             NodeFilter.SHOW_TEXT,
                                             null,
                                             false);
      var node = walker.nextNode();
      while (node) {
        // Perform a search and replace on the text node value.
        var newValue = node.nodeValue.replace(regex, replace);
        if (newValue != node.nodeValue) {
          // The text node has changed so that means we found at least one
          // match.
          found = true;

          // Create a temporary div element and set the innerHTML to the new
          // value.
          div = document.createElement('div');
          div.innerHTML = newValue;

          // Insert all the child nodes of the temporary div element into the
          // document, before the original node.
          child = div.firstChild;
          while (child = div.firstChild) {
            node.parentNode.insertBefore(child, node);
          };

          // Delete the old text node and advance the walker to the next
          // node.
          tmp = node;
          node = walker.nextNode();
          tmp.parentNode.removeChild(tmp);
        } else {
          node = walker.nextNode();
        }
      }

      return found;
    },

    /**
     * Removes all search highlight tags from the document.
     * @private
     */
    unhighlightMatches_: function() {
      // Find all search highlight elements.
      var elements = document.querySelectorAll('.search-highlighted');

      // For each element, remove the highlighting.
      var parent, i;
      for (var i = 0, node; node = elements[i]; i++) {
        parent = node.parentNode;

        // Replace the highlight element with the first child (the text node).
        parent.replaceChild(node.firstChild, node);

        // Normalize the parent so that multiple text nodes will be combined.
        parent.normalize();
      }
    },

    /**
     * Creates a search result bubble attached to an element.
     * @param {Element} element An HTML element, usually a button.
     * @param {string} text A string to show in the bubble.
     * @private
     */
    createSearchBubble_: function(element, text) {
      // avoid appending multiple ballons to a button.
      var sibling = element.previousElementSibling;
      if (sibling && sibling.classList.contains('search-bubble'))
        return;

      var parent = element.parentElement;
      if (parent) {
        var bubble = new SearchBubble(text);
        parent.insertBefore(bubble, element);
        bubble.updatePosition();
      }
    },

    /**
     * Removes all search match bubbles.
     * @private
     */
    removeSearchBubbles_: function() {
      var elements = document.querySelectorAll('.search-bubble');
      var length = elements.length;
      for (var i = 0; i < length; i++)
        elements[i].dispose();
    },

    /**
     * Builds a list of top-level pages to search.  Omits the search page and
     * all sub-pages.
     * @returns {Array} An array of pages to search.
     * @private
     */
    getSearchablePages_: function() {
      var name, page, pages = [];
      for (name in OptionsPage.registeredPages) {
        if (name != this.name) {
          page = OptionsPage.registeredPages[name];
          if (!page.parentPage)
            pages.push(page);
        }
      }
      return pages;
    },

    /**
     * Builds a list of sub-pages (and overlay pages) to search.  Ignore pages
     * that have no associated controls.
     * @returns {Array} An array of pages to search.
     * @private
     */
    getSearchableSubPages_: function() {
      var name, pageInfo, page, pages = [];
      for (name in OptionsPage.registeredPages) {
        page = OptionsPage.registeredPages[name];
        if (page.parentPage && page.associatedSection)
          pages.push(page);
      }
      for (name in OptionsPage.registeredOverlayPages) {
        page = OptionsPage.registeredOverlayPages[name];
        if (page.associatedSection && page.pageDiv != undefined)
          pages.push(page);
      }
      return pages;
    },

    /**
     * A function to handle key press events.
     * @return {Event} a keydown event.
     * @private
     */
    keyDownEventHandler_: function(event) {
      const ESCAPE_KEY_CODE = 27;
      const FORWARD_SLASH_KEY_CODE = 191;

      switch(event.keyCode) {
        case ESCAPE_KEY_CODE:
          if (event.target == this.searchField) {
            this.setSearchText_('');
            this.searchField.blur();
            event.stopPropagation();
            event.preventDefault();
          }
          break;
        case FORWARD_SLASH_KEY_CODE:
          if (!/INPUT|SELECT|BUTTON|TEXTAREA/.test(event.target.tagName)) {
            this.searchField.focus();
            event.stopPropagation();
            event.preventDefault();
          }
          break;
      }
    },
  };

  /**
   * Standardizes a user-entered text query by removing extra whitespace.
   * @param {string} The user-entered text.
   * @return {string} The trimmed query.
   */
  SearchPage.canonicalizeQuery = function(text) {
    // Trim beginning and ending whitespace.
    return text.replace(/^\s+|\s+$/g, '');
  };

  // Export
  return {
    SearchPage: SearchPage
  };

});
<|endoftext|>


<commit_before>module.exports = function(skill, info, bot, message, db) {
    var userData = message.text.match(/\<(.*?)\>/g);
    console.log('messaage text: ' + message.text);
    console.log('userdata' + userData);
  if (userData) {
    // if there is a user is mentioned initiate the challenge!
    //for (var i = 0, len = userData.length; i < len; i++) {
        //FU(userData[i].replace(/[^\w\s]/gi, ''));
        //console.log('userdata[i]' + userData[i]);
        //console.log('userdata[i]' + userData[i].replace(/[^\w\s]/gi, ''));
    //}
    console.log(userData[0].replace(/[^\w\s]/gi, ''));
    var challenger = getSlackName(message.user);//.replace(/[^\w\s]/gi, ''));
    var challenged = getSlackName(userData[0].replace(/[^\w\s]/gi, ''));
    bot.reply(message, challenger + ' has challenged ' + challenged);

    //bot.reply(message,'Let\'s rumble' + userData[0].replace(/[^\w\s]/gi, ''));
  } else {//if no user challenged
    bot.reply(message, 'You didn\'t challenge anyone you chicken shit! :chicken: :poop:');
  }

function getSlackName(userID) {
    bot.api.users.info({user: userID}, (error, response) => {
    console.log('slackname: ' + response.user.real_name);
    return response.user.real_name.toString();
    })

}

function FU(user) {
    //DM some shade!
    bot.api.im.open({ user: user }, function (err, response) {
      if (err) {
        return console.log(err)
      }
    var fuckArray = [
    'https://giphy.com/gifs/cheezburger-olympics-U7P2vnWfPkIQ8',
    'https://giphy.com/gifs/louis-ck-middle-finger-fuck-you-Qd71uU3LzyxXO'
    ];
    var randomFuck = Math.floor(Math.random()* fuckArray.length);

    var dmChannel = response.channel.id
    bot.say({channel: dmChannel, text: 'Someone is throwing shade your way... :middle_finger:'})
    bot.say({channel: dmChannel, text: fuckArray[randomFuck]})
    })

}

  var gameEngine = require('./Rock-Paper-Spock');

    //bot.api.users.info({user: message.user}, (error, response) => {
        //bot.reply(message, 'Rock Paper Scissors (lizard, spock, spiderman, batman, wizard, glock?)');
    //})

  var instance = new gameEngine(gameEngine.DefaultRules);
  instance.addPlayer({id: 1, sign: "spock"});
  instance.addPlayer({id: 2, sign: "rock"});
  instance.addPlayer({id: 3, sign: "spock"});
  instance.addPlayer(4, "rock").addPlayer(5, "paper");
  instance.play();
  console.log(instance.winner);
  //note players that won-lost, add support for W-L records
  //bot.reply(message, 'test successful. maybe code something more useful next time?');
};
<commit_msg>Update RockPaperScissors.js<commit_after>module.exports = function(skill, info, bot, message, db) {
    var userData = message.text.match(/\<(.*?)\>/g);
    console.log('messaage text: ' + message.text);
    console.log('userdata' + userData);
  if (userData) {
    // if there is a user is mentioned initiate the challenge!
    //for (var i = 0, len = userData.length; i < len; i++) {
        //FU(userData[i].replace(/[^\w\s]/gi, ''));
        //console.log('userdata[i]' + userData[i]);
        //console.log('userdata[i]' + userData[i].replace(/[^\w\s]/gi, ''));
    //}
    console.log(userData[0].replace(/[^\w\s]/gi, ''));
    var challenger = getSlackName(message.user);//.replace(/[^\w\s]/gi, ''));
    var challenged = getSlackName(userData[0].replace(/[^\w\s]/gi, ''));
    console.log(challenger);
    console.log(challenged);
    bot.reply(message, challenger + ' has challenged ' + challenged);

    //bot.reply(message,'Let\'s rumble' + userData[0].replace(/[^\w\s]/gi, ''));
  } else {//if no user challenged
    bot.reply(message, 'You didn\'t challenge anyone you chicken shit! :chicken: :poop:');
  }

function getSlackName(userID) {
    bot.api.users.info({user: userID}, (error, response) => {
    console.log('slackname: ' + response.user.real_name);
    return response.user.real_name.toString();
    })

}

function FU(user) {
    //DM some shade!
    bot.api.im.open({ user: user }, function (err, response) {
      if (err) {
        return console.log(err)
      }
    var fuckArray = [
    'https://giphy.com/gifs/cheezburger-olympics-U7P2vnWfPkIQ8',
    'https://giphy.com/gifs/louis-ck-middle-finger-fuck-you-Qd71uU3LzyxXO'
    ];
    var randomFuck = Math.floor(Math.random()* fuckArray.length);

    var dmChannel = response.channel.id
    bot.say({channel: dmChannel, text: 'Someone is throwing shade your way... :middle_finger:'})
    bot.say({channel: dmChannel, text: fuckArray[randomFuck]})
    })

}

  var gameEngine = require('./Rock-Paper-Spock');

    //bot.api.users.info({user: message.user}, (error, response) => {
        //bot.reply(message, 'Rock Paper Scissors (lizard, spock, spiderman, batman, wizard, glock?)');
    //})

  var instance = new gameEngine(gameEngine.DefaultRules);
  instance.addPlayer({id: 1, sign: "spock"});
  instance.addPlayer({id: 2, sign: "rock"});
  instance.addPlayer({id: 3, sign: "spock"});
  instance.addPlayer(4, "rock").addPlayer(5, "paper");
  instance.play();
  console.log(instance.winner);
  //note players that won-lost, add support for W-L records
  //bot.reply(message, 'test successful. maybe code something more useful next time?');
};
<|endoftext|>


<commit_before>/*
 * grunt-lib-phantomjs
 * http://gruntjs.com/
 *
 * Modified for mocha-grunt by Kelly Miyashiro
 *
 * Copyright (c) 2012 "Cowboy" Ben Alman, contributors
 * Licensed under the MIT license.
 */

/*global phantom:true*/

'use strict';

var fs = require('fs');

// The temporary file used for communications.
var tmpfile = phantom.args[0];
// The page .html file to load.
var url = phantom.args[1];
// Extra, optionally overridable stuff.
var options = JSON.parse(phantom.args[2] || {});

// Keep track of the last time a client message was sent.
var last = new Date();

// Messages are sent to the parent by appending them to the tempfile.
var sendMessage = function(arg) {
  var args = Array.isArray(arg) ? arg : [].slice.call(arguments);
  last = new Date();
  fs.write(tmpfile, JSON.stringify(args) + '\n', 'a');
};

// This allows grunt to abort if the PhantomJS version isn't adequate.
sendMessage('private', 'version', phantom.version);

// Abort if the page doesn't send any messages for a while.
setInterval(function() {
  if (new Date() - last > options.timeout) {
    sendMessage('fail.timeout');
    phantom.exit();
  }
}, 100);

// Create a new page.
var page = require('webpage').create();

// Inject bridge script into client page.
var injected;
var inject = function() {
  if (injected) { return; }
  // Inject client-side helper script.
  sendMessage('inject', options.inject);
  page.injectJs(options.inject);
  injected = true;
};

// Keep track if the client-side helper script already has been injected.
page.onUrlChanged = function(newUrl) {
  injected = false;
  sendMessage('onUrlChanged', newUrl);
};

// The client page must send its messages via alert(jsonstring).
page.onAlert = function(str) {
  // The only thing that should ever alert "inject" is the custom event
  // handler this script adds to be executed on DOMContentLoaded.
  if (str === 'inject') {
    inject();
    return;
  }
  // Otherwise, parse the specified message string and send it back to grunt.
  // Unless there's a parse error. Then, complain.
  try {
    sendMessage(JSON.parse(str));
  } catch(err) {
    sendMessage('error.invalidJSON', str);
  }
};

// Relay console logging messages.
page.onConsoleMessage = function(message) {
  sendMessage('console', message);
};

// For debugging.
page.onResourceRequested = function(request) {
  sendMessage('onResourceRequested', request.url);
};

page.onResourceReceived = function(request) {
  if (request.stage === 'end') {
    sendMessage('onResourceReceived', request.url);
  }
};

page.onError = function(msg, trace) {
  sendMessage('error.onError', msg, trace);
};

page.onResourceError = function(resourceError) {
  sendMessage('onResourceError', resourceError.url, resourceError.errorString);
};

// Run before the page is loaded.
page.onInitialized = function() {
  sendMessage('onInitialized');

  // Customization for mocha, passing mocha options from task config
  page.evaluate(function(options) {
    window.PHANTOMJS = options;
  }, options);

  // Abort if there is no bridge to inject.
  if (!options.inject) { return; }
  // Tell the client that when DOMContentLoaded fires, it needs to tell this
  // script to inject the bridge. This should ensure that the bridge gets
  // injected before any other DOMContentLoaded or window.load event handler.
  page.evaluate(function() {
    /*jshint browser:true, devel:true */
    document.addEventListener('DOMContentLoaded', function() {
      alert('inject');
    }, false);
  });
};

// Run when the page has finished loading.
page.onLoadFinished = function(status) {
  // The window has loaded.
  sendMessage('onLoadFinished', status);
  if (status !== 'success') {
    // File loading failure.
    sendMessage('fail.load', url);
    phantom.exit();
  }
};

// Actually load url.
page.open(url);
<commit_msg>Add onResourceTimeout handler.<commit_after>/*
 * grunt-lib-phantomjs
 * http://gruntjs.com/
 *
 * Modified for mocha-grunt by Kelly Miyashiro
 *
 * Copyright (c) 2012 "Cowboy" Ben Alman, contributors
 * Licensed under the MIT license.
 */

/*global phantom:true*/

'use strict';

var fs = require('fs');

// The temporary file used for communications.
var tmpfile = phantom.args[0];
// The page .html file to load.
var url = phantom.args[1];
// Extra, optionally overridable stuff.
var options = JSON.parse(phantom.args[2] || {});

// Keep track of the last time a client message was sent.
var last = new Date();

// Messages are sent to the parent by appending them to the tempfile.
var sendMessage = function(arg) {
  var args = Array.isArray(arg) ? arg : [].slice.call(arguments);
  last = new Date();
  fs.write(tmpfile, JSON.stringify(args) + '\n', 'a');
};

// This allows grunt to abort if the PhantomJS version isn't adequate.
sendMessage('private', 'version', phantom.version);

// Abort if the page doesn't send any messages for a while.
setInterval(function() {
  if (new Date() - last > options.timeout) {
    sendMessage('fail.timeout');
    phantom.exit();
  }
}, 100);

// Create a new page.
var page = require('webpage').create();

// Inject bridge script into client page.
var injected;
var inject = function() {
  if (injected) { return; }
  // Inject client-side helper script.
  sendMessage('inject', options.inject);
  page.injectJs(options.inject);
  injected = true;
};

// Keep track if the client-side helper script already has been injected.
page.onUrlChanged = function(newUrl) {
  injected = false;
  sendMessage('onUrlChanged', newUrl);
};

// The client page must send its messages via alert(jsonstring).
page.onAlert = function(str) {
  // The only thing that should ever alert "inject" is the custom event
  // handler this script adds to be executed on DOMContentLoaded.
  if (str === 'inject') {
    inject();
    return;
  }
  // Otherwise, parse the specified message string and send it back to grunt.
  // Unless there's a parse error. Then, complain.
  try {
    sendMessage(JSON.parse(str));
  } catch(err) {
    sendMessage('error.invalidJSON', str);
  }
};

// Relay console logging messages.
page.onConsoleMessage = function(message) {
  sendMessage('console', message);
};

// For debugging.
page.onResourceRequested = function(request) {
  sendMessage('onResourceRequested', request.url);
};

page.onResourceReceived = function(request) {
  if (request.stage === 'end') {
    sendMessage('onResourceReceived', request.url);
  }
};

page.onError = function(msg, trace) {
  sendMessage('error.onError', msg, trace);
};

page.onResourceError = function(resourceError) {
  sendMessage('onResourceError', resourceError.url, resourceError.errorString);
};

page.onResourceTimeout = function(request) {
  sendMessage('onResourceTimeout', request.url, request.errorString);
};

// Run before the page is loaded.
page.onInitialized = function() {
  sendMessage('onInitialized');

  // Customization for mocha, passing mocha options from task config
  page.evaluate(function(options) {
    window.PHANTOMJS = options;
  }, options);

  // Abort if there is no bridge to inject.
  if (!options.inject) { return; }
  // Tell the client that when DOMContentLoaded fires, it needs to tell this
  // script to inject the bridge. This should ensure that the bridge gets
  // injected before any other DOMContentLoaded or window.load event handler.
  page.evaluate(function() {
    /*jshint browser:true, devel:true */
    document.addEventListener('DOMContentLoaded', function() {
      alert('inject');
    }, false);
  });
};

// Run when the page has finished loading.
page.onLoadFinished = function(status) {
  // The window has loaded.
  sendMessage('onLoadFinished', status);
  if (status !== 'success') {
    // File loading failure.
    sendMessage('fail.load', url);
    phantom.exit();
  }
};

// Actually load url.
page.open(url);
<|endoftext|>


<commit_before>(function($) {
	function changeMultiple(method, arg) {
		// Apply the same method and arg to multiple objects
		return function() {
			var args = arguments;
			if(!args.length) return false; // no args
			for(var i = 0; i < args.length; i++) {
				if(args[i] && args[i][method]) args[i][method](arg);
			}
			return true;
		};
	}
	var show = changeMultiple("removeClass", "jshidden");
	var hide = changeMultiple("addClass", "jshidden");
	function visible(el) {
		return (el ? !el.hasClass("jshidden") : null);
	}
	function MenuLabel(label) {
		// Object for manipulating menulabel
		label.click(function(e) {
			e.preventDefault();
		});
		var labeltext;
		function init() {
			labeltext = label.contents().get(0).nodeValue;
		}
		this.obj = label;
		this.addColon = function() {
			// Show colon in label when expanded
			init();
			if(labeltext.indexOf(":") == -1) {
				label.contents().get(0).nodeValue = $.trim(labeltext) + ":";
			}
		};
		this.removeColon = function() {
			// Remove colon in label when collapsed
			init();
			label.contents().get(0).nodeValue = labeltext.replace(":", "");
		};
		this.removeColon();
	}
	function RichSelect(el) {
		// Object for rich menu functionality
		var rs = this;
		function MenuBox(menubox) {
			// Add empty element for holding option text
			menubox.prepend("<span />");
			var menutext = menubox.children().first();
			var richopt = menubox.find(".richopt");
			hide(menubox, richopt);
			// Add remove button
			menubox.append($("<span />", {
				"class" : "removeval",
				click : function() {
					rs.removeVal();
				}
			}));
			menubox.children().last().html($("<img />", {
				src : "remove.png",
				alt : "remove"
			}));
			this.update = function(option) {
				// Shows selected option. If option is a richopt show that element.
				rs.menulabel.addColon();
				if(option.hasClass("richopt")) {
					menutext.text("");
					show(richopt, menubox);
				}
				else {
					if(!richopt.hasClass("jshidden")) hide(richopt);
					menutext.text(option.text());
					show(menubox);
				}
				rs.menu.position();
			};
			this.show = function() {
				rs.menulabel.addColon();
				show(menubox);
			};
			this.hide = function() {
				rs.menulabel.removeColon();
				hide(menubox);
			};
			this.reset = function() {
				this.hide();
				menutext.text("");
			};
			this.focus = function() {
				if(!visible(richopt)) return false;
				var inputs = richopt.find("input");
				// Select form element for added usability
				if(inputs.size()) {
					inputs.first().select();
				}
				return true;
			};
		}
		function Menu(menu) {
			var self = this;
			menu = (menu && menu.size()) ? menu : null; // Check for existence of menu
			this.name = menu ? menu.attr("name") : "";
			this.position = function() {
				// Set menu to show below button
				if(menu) menu.css("top", el.innerHeight() - 2);
			};
			this.visible = function() {
				return menu ? visible(menu) : false;
			};
			this.show = function() {
				if(menu) {
					show(menu);
					menu.focus();
				}
				else rs.menubox.show();
			};
			this.hide = function() {
				if(menu) {
					hide(menu);
					rs.menubox.focus();
				}
				else rs.menubox.hide();
			};
			this.update = function() {
				// Update selected menu option from valstore value
				if(!menu) return false;
				var option;
				var found = false;
				if(rs.valstore.exists()) {
					menu.children().each(function(i, el) {
						option = $(el);
						if(option.val() === rs.valstore.val()) {
							self.selected(option);
							found = true;
							return false;
						}
					});
				}
				else {
					option = this.selected();
					if(option) found = true;
				}
				if(found) {
					rs.menubox.update(option);
				}
				return found;
			};
			this.selected = function(o) {
				if(!menu) return null;
				if(o) {
					o.attr("selected", "selected");
					return o;
				}
				o = menu.children("option:selected");
				if(!o.size()) o = null;
				return o;
			};
			this.reset = function() {
				if(!menu) return false;
				menu.get(0).selectedIndex = -1;
				hide(menu);
				this.position();
				return true;
			};
			this.val = function() {
				var val = "";
				var o = menu.selected();
				if(o) val = o.val();
				return val;
			};
			if(menu) {
				// Event handlers keep menu and valstore in sync
				menu.click(function(e) {
					rs.valstore.update();
					// The richselect click handler hides the menu
				});
				menu.change(function(e) {
					rs.valstore.update();
				});
				menu.jkey('enter', function() {
					rs.valstore.update();
					self.hide();
				});
				// Prevent pressing enter in the menu from submitting the form
				menu.keypress(function(e) {
					if(e.which == 13) e.preventDefault();
				});
				hide(menu);
				// Make into multiline select
				menu.attr("size", menu.children().size());
				this.position();
			}
		}
		function Valstore(valstore) {
			valstore = valstore.size() ? valstore : null; // Check for existence of valstore
			this.name = valstore ? valstore.attr("name") : "";
			this.exists = function() {
				return valstore ? true : false;
			};
			this.val = function() {
				return valstore ? valstore.val() : "";
			};
			this.update = function() {
				// Update valstore from selected menu option
				if(!valstore) return false;
				var option = rs.menu.selected();
				if(option) {
					rs.menubox.update(option);
					valstore.val(option.val());
				}
				return true;
			};
			this.reset = function() {
				if(!valstore) return false;
				valstore.val("");
				return true;
			};
			if(valstore) {
				valstore.change(function() {
					rs.menu.update();
				});
			}
		}
		this.getVal = function() {
			var val;
			if(rs.valstore.exists()) {
				val = rs.valstore.val();
			}
			else {
				val = rs.menu.val();
			}
			return val;
		};
		this.removeVal = function() {
			// Remove value and collapse menu button to initial form
			rs.menubox.reset();
			rs.valstore.reset();
			rs.menu.reset();
		};
		this.menulabel = new MenuLabel(el.find(".showmenu"));
		this.menubox = new MenuBox(el.find(".menubox"));
		this.menu = new Menu(el.find(".menu"));
		this.valstore = new Valstore(el.find(".valstore"));
		this.menu.update();
		this.name = (this.valstore.exists() ? this.valstore.name : this.menu.name);
		// Make text unselectable in IE and Opera
		if(typeof el.get(0).unselectable !== "undefined") {
			el.get(0).unselectable = "on";
			el.find("*").each(function(i, el) {
				if(!$(el).is("input, select, textarea")) el.unselectable = "on";
			});
		}
		// Event handlers for menu button
		el.click(function(e) {
			var target = $(e.target);
			if(target.is(".menubox label") || target.is("input") || target.is(".removeval")) {
				return;
			}
			if(rs.menu.visible())
				rs.menu.hide();
			else
				rs.menu.show();
			e.stopPropagation(); // Don't let the document close the menu again
		});
		el.jkey('space', function() {
			rs.menu.show();
		});
		// Add dropdown image
		el.append($("<img />", {
			src : "dropdown.png",
			alt : "dropdown"
		}));
	}
	// Array holding all rich menus
	var selects = [];
	$(function() {
		// Close any open menu
		$(document).click(function() {
			for(var i = 0; i < selects.length; i++) {
				if(selects[i].menu.visible()) selects[i].menu.hide();
			}
		});
	});
	// jQuery plugin
	$.richselect = $.richselect || function(selector, callback) {
		var a = [];
		$(selector).each(function(i, el) {
			var select = new RichSelect($(el));
			a[a.length] = select;
			selects[selects.length] = select;
			if(typeof callback === "function") callback(select);
		});
		return a;
	};
	$.richselect.selects = $.richselect.selects || selects;
})(jQuery);<commit_msg>Add support for callback function for removeVal.<commit_after>(function($) {
	function changeMultiple(method, arg) {
		// Apply the same method and arg to multiple objects
		return function() {
			var args = arguments;
			if(!args.length) return false; // no args
			for(var i = 0; i < args.length; i++) {
				if(args[i] && args[i][method]) args[i][method](arg);
			}
			return true;
		};
	}
	var show = changeMultiple("removeClass", "jshidden");
	var hide = changeMultiple("addClass", "jshidden");
	function visible(el) {
		return (el ? !el.hasClass("jshidden") : null);
	}
	function MenuLabel(label) {
		// Object for manipulating menulabel
		label.click(function(e) {
			e.preventDefault();
		});
		var labeltext;
		function init() {
			labeltext = label.contents().get(0).nodeValue;
		}
		this.obj = label;
		this.addColon = function() {
			// Show colon in label when expanded
			init();
			if(labeltext.indexOf(":") == -1) {
				label.contents().get(0).nodeValue = $.trim(labeltext) + ":";
			}
		};
		this.removeColon = function() {
			// Remove colon in label when collapsed
			init();
			label.contents().get(0).nodeValue = labeltext.replace(":", "");
		};
		this.removeColon();
	}
	function RichSelect(el, opts) {
		// Object for rich menu functionality
		opts = opts || {};
		var rs = this;
		function MenuBox(menubox) {
			// Add empty element for holding option text
			menubox.prepend("<span />");
			var menutext = menubox.children().first();
			var richopt = menubox.find(".richopt");
			hide(menubox, richopt);
			// Add remove button
			menubox.append($("<span />", {
				"class" : "removeval",
				click : function() {
					rs.removeVal();
				}
			}));
			menubox.children().last().html($("<img />", {
				src : "remove.png",
				alt : "remove"
			}));
			this.update = function(option) {
				// Shows selected option. If option is a richopt show that element.
				rs.menulabel.addColon();
				if(option.hasClass("richopt")) {
					menutext.text("");
					show(richopt, menubox);
				}
				else {
					if(!richopt.hasClass("jshidden")) hide(richopt);
					menutext.text(option.text());
					show(menubox);
				}
				rs.menu.position();
			};
			this.show = function() {
				rs.menulabel.addColon();
				show(menubox);
			};
			this.hide = function() {
				rs.menulabel.removeColon();
				hide(menubox);
			};
			this.reset = function() {
				this.hide();
				menutext.text("");
			};
			this.focus = function() {
				if(!visible(richopt)) return false;
				var inputs = richopt.find("input");
				// Select form element for added usability
				if(inputs.size()) {
					inputs.first().select();
				}
				return true;
			};
		}
		function Menu(menu) {
			var self = this;
			menu = (menu && menu.size()) ? menu : null; // Check for existence of menu
			this.name = menu ? menu.attr("name") : "";
			this.position = function() {
				// Set menu to show below button
				if(menu) menu.css("top", el.innerHeight() - 2);
			};
			this.visible = function() {
				return menu ? visible(menu) : false;
			};
			this.show = function() {
				if(menu) {
					show(menu);
					menu.focus();
				}
				else rs.menubox.show();
			};
			this.hide = function() {
				if(menu) {
					hide(menu);
					rs.menubox.focus();
				}
				else rs.menubox.hide();
			};
			this.update = function() {
				// Update selected menu option from valstore value
				if(!menu) return false;
				var option;
				var found = false;
				if(rs.valstore.exists()) {
					menu.children().each(function(i, el) {
						option = $(el);
						if(option.val() === rs.valstore.val()) {
							self.selected(option);
							found = true;
							return false;
						}
					});
				}
				else {
					option = this.selected();
					if(option) found = true;
				}
				if(found) {
					rs.menubox.update(option);
				}
				return found;
			};
			this.selected = function(o) {
				if(!menu) return null;
				if(o) {
					o.attr("selected", "selected");
					return o;
				}
				o = menu.children("option:selected");
				if(!o.size()) o = null;
				return o;
			};
			this.reset = function() {
				if(!menu) return false;
				menu.get(0).selectedIndex = -1;
				hide(menu);
				this.position();
				return true;
			};
			this.val = function() {
				var val = "";
				var o = menu.selected();
				if(o) val = o.val();
				return val;
			};
			if(menu) {
				// Event handlers keep menu and valstore in sync
				menu.click(function(e) {
					rs.valstore.update();
					// The richselect click handler hides the menu
				});
				menu.change(function(e) {
					rs.valstore.update();
				});
				menu.jkey('enter', function() {
					rs.valstore.update();
					self.hide();
				});
				// Prevent pressing enter in the menu from submitting the form
				menu.keypress(function(e) {
					if(e.which == 13) e.preventDefault();
				});
				hide(menu);
				// Make into multiline select
				menu.attr("size", menu.children().size());
				this.position();
			}
		}
		function Valstore(valstore) {
			valstore = valstore.size() ? valstore : null; // Check for existence of valstore
			this.name = valstore ? valstore.attr("name") : "";
			this.exists = function() {
				return valstore ? true : false;
			};
			this.val = function() {
				return valstore ? valstore.val() : "";
			};
			this.update = function() {
				// Update valstore from selected menu option
				if(!valstore) return false;
				var option = rs.menu.selected();
				if(option) {
					rs.menubox.update(option);
					valstore.val(option.val());
				}
				return true;
			};
			this.reset = function() {
				if(!valstore) return false;
				valstore.val("");
				return true;
			};
			if(valstore) {
				valstore.change(function() {
					rs.menu.update();
				});
			}
		}
		this.getVal = function() {
			var val;
			if(rs.valstore.exists()) {
				val = rs.valstore.val();
			}
			else {
				val = rs.menu.val();
			}
			return val;
		};
		this.removeVal = function() {
			// Call event and if it returns false cancel
			if(typeof opts.removeVal === "function") {
				if(!opts.removeVal(rs)) return false;
			}
			// Remove value and collapse menu button to initial form
			rs.menubox.reset();
			rs.valstore.reset();
			rs.menu.reset();
			return true;
		};
		this.menulabel = new MenuLabel(el.find(".showmenu"));
		this.menubox = new MenuBox(el.find(".menubox"));
		this.menu = new Menu(el.find(".menu"));
		this.valstore = new Valstore(el.find(".valstore"));
		this.menu.update();
		this.name = (this.valstore.exists() ? this.valstore.name : this.menu.name);
		// Make text unselectable in IE and Opera
		if(typeof el.get(0).unselectable !== "undefined") {
			el.get(0).unselectable = "on";
			el.find("*").each(function(i, el) {
				if(!$(el).is("input, select, textarea")) el.unselectable = "on";
			});
		}
		// Event handlers for menu button
		el.click(function(e) {
			var target = $(e.target);
			if(target.is(".menubox label") || target.is("input") || target.is(".removeval")) {
				return;
			}
			if(rs.menu.visible())
				rs.menu.hide();
			else
				rs.menu.show();
			e.stopPropagation(); // Don't let the document close the menu again
		});
		el.jkey('space', function() {
			rs.menu.show();
		});
		// Add dropdown image
		el.append($("<img />", {
			src : "dropdown.png",
			alt : "dropdown"
		}));
	}
	// Array holding all rich menus
	var selects = [];
	$(function() {
		// Close any open menu
		$(document).click(function() {
			for(var i = 0; i < selects.length; i++) {
				if(selects[i].menu.visible()) selects[i].menu.hide();
			}
		});
	});
	// jQuery plugin
	$.richselect = $.richselect || function(selector, opts, callback) {
		opts = opts || {};
		var a = [];
		$(selector).each(function(i, el) {
			var select = new RichSelect($(el), opts);
			a[a.length] = select;
			selects[selects.length] = select;
			if(typeof callback === "function") callback(select);
		});
		return a;
	};
	$.richselect.selects = $.richselect.selects || selects;
})(jQuery);<|endoftext|>