<commit_before>import date from "fields/date"
import impulse from "lib/loaders/impulse"
import gravity from "lib/loaders/gravity"
import { get } from "lodash"
import { GraphQLBoolean, GraphQLList, GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLEnumType } from "graphql"
import { ArtworkType } from "schema/artwork"
const { IMPULSE_APPLICATION_ID } = process.env

export const BuyerOutcomeTypes = new GraphQLEnumType({
  name: "BuyerOutcomeTypes",
  values: {
    PURCHASED: {
      value: "purchased",
    },
    STILL_CONSIDERING: {
      value: "still_considering",
    },
    HIGH_PRICE: {
      value: "high_price",
    },
    LOST_INTEREST: {
      value: "lost_interest",
    },
    WORK_UNAVAILABLE: {
      value: "work_unavailable",
    },
    OTHER: {
      value: "other",
    },
    BLANK: {
      value: "",
    },
  },
})

export const MessageType = new GraphQLObjectType({
  name: "MessageType",
  description: "A message in a conversation.",
  fields: {
    id: {
      description: "Impulse id.",
      type: GraphQLString,
    },
    from_email_address: {
      description: "Email address of sender.",
      type: GraphQLString,
    },
    snippet: {
      description: "A snippet of the full message",
      type: GraphQLString,
    },
  },
})

export const ConversationFields = {
  id: {
    description: "Impulse id.",
    type: GraphQLString,
  },
  inquiry_id: {
    description: "Gravity inquiry id.",
    type: GraphQLString,
  },
  from_id: {
    type: new GraphQLNonNull(GraphQLString),
  },
  from_type: {
    type: new GraphQLNonNull(GraphQLString),
  },
  from_name: {
    type: new GraphQLNonNull(GraphQLString),
  },
  from_email: {
    type: new GraphQLNonNull(GraphQLString),
  },
  to_id: {
    type: new GraphQLNonNull(GraphQLString),
  },
  to_type: {
    type: new GraphQLNonNull(GraphQLString),
  },
  to_name: {
    type: new GraphQLNonNull(GraphQLString),
  },
  buyer_outcome: {
    type: GraphQLString,
  },
  buyer_outcome_at: date,
  created_at: date,
  purchase_request: {
    type: GraphQLBoolean,
  },
  initial_message: {
    type: GraphQLString,
  },
  last_message: {
    type: GraphQLString,
    resolve: conversation => {
      return get(conversation, "_embedded.last_message.snippet")
    },
  },
  last_message_at: date,

  artworks: {
    type: new GraphQLList(ArtworkType),
    resolve: conversation => {
      const ids = []

      for (const item of conversation.items) {
        if (item.item_type === "Artwork") {
          ids.push(item.item_id)
        }
      }

      return gravity("artworks", { ids })
    },
  },

  messages: {
    type: new GraphQLList(MessageType),
  },
}

export const ConversationType = new GraphQLObjectType({
  name: "ConversationType",
  description: "A conversation.",
  fields: ConversationFields,
})

export default {
  type: ConversationType,
  description: "A conversation, usually between a user and a partner",
  args: {
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: "The ID of the Conversation",
    },
  },
  resolve: (root, { id }, request, { rootValue: { accessToken } }) => {
    if (!accessToken) return null
    return gravity
      .with(accessToken, { method: "POST" })("me/token", {
        client_application_id: IMPULSE_APPLICATION_ID,
      })
      .then(data => {
        return impulse
          .with(data.token, { method: "GET" })(`conversations/${id}`, {
            expand: ["messages"],
          })
          .then(impulseData => {
            return impulseData
          })
      })
  },
}
<commit_msg>[MSG] Made certain message fields non-null'<commit_after>import date from "fields/date"
import impulse from "lib/loaders/impulse"
import gravity from "lib/loaders/gravity"
import { get } from "lodash"
import { GraphQLBoolean, GraphQLList, GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLEnumType } from "graphql"
import { ArtworkType } from "schema/artwork"
const { IMPULSE_APPLICATION_ID } = process.env

export const BuyerOutcomeTypes = new GraphQLEnumType({
  name: "BuyerOutcomeTypes",
  values: {
    PURCHASED: {
      value: "purchased",
    },
    STILL_CONSIDERING: {
      value: "still_considering",
    },
    HIGH_PRICE: {
      value: "high_price",
    },
    LOST_INTEREST: {
      value: "lost_interest",
    },
    WORK_UNAVAILABLE: {
      value: "work_unavailable",
    },
    OTHER: {
      value: "other",
    },
    BLANK: {
      value: "",
    },
  },
})

export const MessageType = new GraphQLObjectType({
  name: "MessageType",
  description: "A message in a conversation.",
  fields: {
    id: {
      description: "Impulse id.",
      type: new GraphQLNonNull(GraphQLString),
    },
    from_email_address: {
      description: "Email address of sender.",
      type: new GraphQLNonNull(GraphQLString),
    },
    snippet: {
      description: "A snippet of the full message",
      type: new GraphQLNonNull(GraphQLString),
    },
  },
})

export const ConversationFields = {
  id: {
    description: "Impulse id.",
    type: GraphQLString,
  },
  inquiry_id: {
    description: "Gravity inquiry id.",
    type: GraphQLString,
  },
  from_id: {
    type: new GraphQLNonNull(GraphQLString),
  },
  from_type: {
    type: new GraphQLNonNull(GraphQLString),
  },
  from_name: {
    type: new GraphQLNonNull(GraphQLString),
  },
  from_email: {
    type: new GraphQLNonNull(GraphQLString),
  },
  to_id: {
    type: new GraphQLNonNull(GraphQLString),
  },
  to_type: {
    type: new GraphQLNonNull(GraphQLString),
  },
  to_name: {
    type: new GraphQLNonNull(GraphQLString),
  },
  buyer_outcome: {
    type: GraphQLString,
  },
  buyer_outcome_at: date,
  created_at: date,
  purchase_request: {
    type: GraphQLBoolean,
  },
  initial_message: {
    type: new GraphQLNonNull(GraphQLString),
  },
  last_message: {
    type: new GraphQLNonNull(GraphQLString),
    resolve: conversation => {
      return get(conversation, "_embedded.last_message.snippet")
    },
  },
  last_message_at: date,

  artworks: {
    type: new GraphQLList(ArtworkType),
    resolve: conversation => {
      const ids = []

      for (const item of conversation.items) {
        if (item.item_type === "Artwork") {
          ids.push(item.item_id)
        }
      }

      return gravity("artworks", { ids })
    },
  },

  messages: {
    type: new GraphQLList(MessageType),
  },
}

export const ConversationType = new GraphQLObjectType({
  name: "ConversationType",
  description: "A conversation.",
  fields: ConversationFields,
})

export default {
  type: ConversationType,
  description: "A conversation, usually between a user and a partner",
  args: {
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: "The ID of the Conversation",
    },
  },
  resolve: (root, params, request, { rootValue: { accessToken } }) => {
    if (!accessToken) return null
    return gravity
      .with(accessToken, { method: "POST" })("me/token", {
        client_application_id: IMPULSE_APPLICATION_ID,
      })
      .then(data => {
        params.expand = ["messages"]
        return impulse.with(data.token, { method: "GET" })(`conversations/${params.id}`, params).then(impulseData => {
          return impulseData
        })
      })
  },
}
<|endoftext|>


<commit_before>define([
  './mixin',
  './event-emitter',
  './event-listener',
], function (
  mixin,
  eventEmitter,
  eventListener
) {
  'use strict';

  var PRECHANGE_EVENT = 'prechange';
  var CHANGE_EVENT = 'change';

  function validateInput(val, propName, propSchema) {
    var type = typeof val;
    if (propSchema.type && !(type === propSchema.type || type === 'undefined')) {
      throw 'Property ' + propName + ' is of type ' + propSchema.type +
          ' and can not be assigned a value of type ' + type;
    }
    if (propSchema.forbiddenValues && propSchema.forbiddenValues.indexOf(val) !== -1) {
      throw 'Property ' + propName + ' is not allowed to be ' + val;
    }
  }

  function makeSetter(propName, propSchema) {
    var MY_PRECHANGE_EVENT = 'prechange:' + propName;
    var MY_CHANGE_EVENT = 'change:' + propName;

    // TODO: make two different setter functions (in outer scope) and select (+bind)
    // one of the depending of whether the propery has any dependers
    return function setter(canonicalVal) {
      // If there is a custom setter, use it to transform the value
      propSchema.setter && (canonicalVal = propSchema.setter.call(this, canonicalVal));

      // Input validation
      validateInput(canonicalVal, propName, propSchema);

      // Do a "dry-run" to test if the value would actually change
      var oldVal = this[propName];
      var oldCanonicalVal = this.__private.values[propName];
      this.__private.values[propName] = canonicalVal;

      // If the value change did not affect the public value, we don't trigger any events
      if (this[propName] === oldVal) { return; }

      // Switch the value back and do things for real
      this.__private.values[propName] = oldCanonicalVal;

      this.emit(PRECHANGE_EVENT, [ propName, oldVal, propName, canonicalVal, this ]);
      this.emit(MY_PRECHANGE_EVENT, [ oldVal, propName, canonicalVal, this ]);

      this.__private.values[propName] = canonicalVal;

      var newVal = this[propName];
      this.emit(CHANGE_EVENT, [ propName, newVal, oldVal, this ]);
      this.emit(MY_CHANGE_EVENT, [ newVal, oldVal, this ]);
    };
  }

  function makeGetter(propName, propSchema) {
    return propSchema.getter ?
        function getter() { return propSchema.getter.call(this, this.__private.values[propName]); } :
        function getter() { return this.__private.values[propName]; };
  }

  var triggerDependerPrechangeEvents = function (dependerPropNames, currentVal, triggeringPropName, canonicalVal) {
    for (var i = 0; i < dependerPropNames.length; ++i) {
      var propName = dependerPropNames[i];
      // Do a "dry-run" to test if the value would actually change
      var oldVal = this.__private.previousValues[propName] = this[propName];

      var oldCanonicalTriggeringPropVal = this.__private.values[triggeringPropName];
      this.__private.values[triggeringPropName] = canonicalVal;

      // If the value change did not affect the depender's public value, we don't trigger any events
      var noChange = this[propName] === oldVal;

      this.__private.values[triggeringPropName] = oldCanonicalTriggeringPropVal;

      if (noChange) { continue; }

      this.emit(PRECHANGE_EVENT, [ propName, oldVal, triggeringPropName, canonicalVal, this ]);
      this.emit(PRECHANGE_EVENT + ':' + propName, [ oldVal, triggeringPropName, canonicalVal, this ]); // TODO: optimize
    }
  };

  var triggerDependerChangeEvents = function (dependerPropNames) {
    for (var i = 0; i < dependerPropNames.length; ++i) {
      var propName = dependerPropNames[i];
      var prevValue = this.__private.previousValues[propName];
      var value = this[propName];

      if (value === prevValue) { continue; }

      this.emit(CHANGE_EVENT, [ propName, value, prevValue, this ]);
      this.emit(CHANGE_EVENT + ':' + propName, [ value, prevValue, this ]); // TODO: optimize
    }
  };

  function Record(schema, values) {
    Object.defineProperty(this, '__private', { writable: true, value: {}, });
    this.__private.values = {};
    this.__private.previousValues = {};

    values = values || {};
    schema = schema || {};

    var property, propertySchema;
    var propertiesUnion = {};

    // Set up dependers and remove dependencies
    for (property in schema) {
      propertySchema = schema[property] = schema[property] || {};

      if (!propertySchema.dependencies) { continue; }

      while (propertySchema.dependencies.length) {
        var dependency = propertySchema.dependencies.pop();
        var dependencySchema = schema[dependency] = schema[dependency] || {};
        dependencySchema.dependers = dependencySchema.dependers || [];
        dependencySchema.dependers.push(property);
      }
      delete propertySchema.dependencies;
    }

    for (property in schema) {
      propertySchema = schema[property];
      typeof propertySchema === 'string' && (propertySchema = schema[property] = { type: propertySchema });

      var descriptor = {
        enumerable: true,
      };
      if (propertySchema.getter !== false) {
        descriptor.get = makeGetter(property, propertySchema);
      }
      if (propertySchema.setter !== false) {
        descriptor.set = makeSetter(property, propertySchema);
      }
      Object.defineProperty(this, property, descriptor);

      if (propertySchema.dependers) {
        this.listenTo(this, 'prechange:' + property,
            triggerDependerPrechangeEvents.bind(this, propertySchema.dependers));
        this.listenTo(this, 'change:' + property,
            triggerDependerChangeEvents.bind(this, propertySchema.dependers));
      }

      propertiesUnion[property] = propertySchema.defaultValue;
    }

    // Disabled, for now
    // Object.preventExtensions(this);

    for (property in values) {
      if (!schema.hasOwnProperty(property)) {
        throw 'Cannot assign undeclared property ' + property;
      }
      propertiesUnion[property] = values[property];
    }

    // Silently assign initial values
    for (property in propertiesUnion) {
      var val = propertiesUnion[property];
      schema[property].setter && (val = schema[property].setter.call(this, val));
      validateInput(val, property, schema[property]);
      this.__private.values[property] = val;
    }
  }

  // toJSON() actually returns an object, which is a bit misleading. For compatibility reasons.
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON_behavior
  Record.prototype.toJSON = function () {
    return JSON.parse(this.toJSONString());
  };

  Record.prototype.toJSONString = Record.prototype.toString = function () {
    var obj = {};
    for (var property in this.__private.values) {
      obj[property] = this[property];
    }
    return JSON.stringify(obj);
  };

  mixin(Record, eventEmitter);
  mixin(Record, eventListener);

  return Record;

});
<commit_msg>Rename property -> propName, which is more correct<commit_after>define([
  './mixin',
  './event-emitter',
  './event-listener',
], function (
  mixin,
  eventEmitter,
  eventListener
) {
  'use strict';

  var PRECHANGE_EVENT = 'prechange';
  var CHANGE_EVENT = 'change';

  function validateInput(val, propName, propSchema) {
    var type = typeof val;
    if (propSchema.type && !(type === propSchema.type || type === 'undefined')) {
      throw 'Property ' + propName + ' is of type ' + propSchema.type +
          ' and can not be assigned a value of type ' + type;
    }
    if (propSchema.forbiddenValues && propSchema.forbiddenValues.indexOf(val) !== -1) {
      throw 'Property ' + propName + ' is not allowed to be ' + val;
    }
  }

  function makeSetter(propName, propSchema) {
    var MY_PRECHANGE_EVENT = 'prechange:' + propName;
    var MY_CHANGE_EVENT = 'change:' + propName;

    // TODO: make two different setter functions (in outer scope) and select (+bind)
    // one of the depending of whether the propery has any dependers
    return function setter(canonicalVal) {
      // If there is a custom setter, use it to transform the value
      propSchema.setter && (canonicalVal = propSchema.setter.call(this, canonicalVal));

      // Input validation
      validateInput(canonicalVal, propName, propSchema);

      // Do a "dry-run" to test if the value would actually change
      var oldVal = this[propName];
      var oldCanonicalVal = this.__private.values[propName];
      this.__private.values[propName] = canonicalVal;

      // If the value change did not affect the public value, we don't trigger any events
      if (this[propName] === oldVal) { return; }

      // Switch the value back and do things for real
      this.__private.values[propName] = oldCanonicalVal;

      this.emit(PRECHANGE_EVENT, [ propName, oldVal, propName, canonicalVal, this ]);
      this.emit(MY_PRECHANGE_EVENT, [ oldVal, propName, canonicalVal, this ]);

      this.__private.values[propName] = canonicalVal;

      var newVal = this[propName];
      this.emit(CHANGE_EVENT, [ propName, newVal, oldVal, this ]);
      this.emit(MY_CHANGE_EVENT, [ newVal, oldVal, this ]);
    };
  }

  function makeGetter(propName, propSchema) {
    return propSchema.getter ?
        function getter() { return propSchema.getter.call(this, this.__private.values[propName]); } :
        function getter() { return this.__private.values[propName]; };
  }

  var triggerDependerPrechangeEvents = function (dependerPropNames, currentVal, triggeringPropName, canonicalVal) {
    for (var i = 0; i < dependerPropNames.length; ++i) {
      var propName = dependerPropNames[i];
      // Do a "dry-run" to test if the value would actually change
      var oldVal = this.__private.previousValues[propName] = this[propName];

      var oldCanonicalTriggeringPropVal = this.__private.values[triggeringPropName];
      this.__private.values[triggeringPropName] = canonicalVal;

      // If the value change did not affect the depender's public value, we don't trigger any events
      var noChange = this[propName] === oldVal;

      this.__private.values[triggeringPropName] = oldCanonicalTriggeringPropVal;

      if (noChange) { continue; }

      this.emit(PRECHANGE_EVENT, [ propName, oldVal, triggeringPropName, canonicalVal, this ]);
      this.emit(PRECHANGE_EVENT + ':' + propName, [ oldVal, triggeringPropName, canonicalVal, this ]); // TODO: optimize
    }
  };

  var triggerDependerChangeEvents = function (dependerPropNames) {
    for (var i = 0; i < dependerPropNames.length; ++i) {
      var propName = dependerPropNames[i];
      var prevValue = this.__private.previousValues[propName];
      var value = this[propName];

      if (value === prevValue) { continue; }

      this.emit(CHANGE_EVENT, [ propName, value, prevValue, this ]);
      this.emit(CHANGE_EVENT + ':' + propName, [ value, prevValue, this ]); // TODO: optimize
    }
  };

  function Record(schema, values) {
    Object.defineProperty(this, '__private', { writable: true, value: {}, });
    this.__private.values = {};
    this.__private.previousValues = {};

    values = values || {};
    schema = schema || {};

    var propName, propertySchema;
    var propertiesUnion = {};

    // Set up dependers and remove dependencies
    for (propName in schema) {
      propertySchema = schema[propName] = schema[propName] || {};

      if (!propertySchema.dependencies) { continue; }

      while (propertySchema.dependencies.length) {
        var dependency = propertySchema.dependencies.pop();
        var dependencySchema = schema[dependency] = schema[dependency] || {};
        dependencySchema.dependers = dependencySchema.dependers || [];
        dependencySchema.dependers.push(propName);
      }
      delete propertySchema.dependencies;
    }

    for (propName in schema) {
      propertySchema = schema[propName];
      typeof propertySchema === 'string' && (propertySchema = schema[propName] = { type: propertySchema });

      var descriptor = {
        enumerable: true,
      };
      if (propertySchema.getter !== false) {
        descriptor.get = makeGetter(propName, propertySchema);
      }
      if (propertySchema.setter !== false) {
        descriptor.set = makeSetter(propName, propertySchema);
      }
      Object.defineProperty(this, propName, descriptor);

      if (propertySchema.dependers) {
        this.listenTo(this, 'prechange:' + propName,
            triggerDependerPrechangeEvents.bind(this, propertySchema.dependers));
        this.listenTo(this, 'change:' + propName,
            triggerDependerChangeEvents.bind(this, propertySchema.dependers));
      }

      propertiesUnion[propName] = propertySchema.defaultValue;
    }

    // Disabled, for now
    // Object.preventExtensions(this);

    for (propName in values) {
      if (!schema.hasOwnProperty(propName)) {
        throw 'Cannot assign undeclared property ' + propName;
      }
      propertiesUnion[propName] = values[propName];
    }

    // Silently assign initial values
    for (propName in propertiesUnion) {
      var val = propertiesUnion[propName];
      schema[propName].setter && (val = schema[propName].setter.call(this, val));
      validateInput(val, propName, schema[propName]);
      this.__private.values[propName] = val;
    }
  }

  // toJSON() actually returns an object, which is a bit misleading. For compatibility reasons.
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON_behavior
  Record.prototype.toJSON = function () {
    return JSON.parse(this.toJSONString());
  };

  Record.prototype.toJSONString = Record.prototype.toString = function () {
    var keys = Object.keys(this);
    var obj = {};
    for (var i = 0; i < keys.length; ++i) {
      var propName = keys[i];
      obj[propName] = this[propName];
    }
    return JSON.stringify(obj);
  };

  mixin(Record, eventEmitter);
  mixin(Record, eventListener);

  return Record;

});
<|endoftext|>


<commit_before>export { default as add }         from './add'
export { default as compose }     from './compose'
export { default as curry }       from './curry'
export { default as curryN }      from './curryN'
export { default as filter }      from './filter'
export { default as findIndex }   from './findIndex'
export { default as flatMap }     from './flatMap'
export { default as flatten }     from './flatten'
export { default as forEach }     from './forEach'
export { default as head }        from './head'
export { default as indexOf }     from './indexOf'
export { default as keys }        from './keys'
export { default as last }        from './last'
export { default as map }         from './map'
export { default as merge }       from './merge'
export { default as pipe }        from './pipe'
export { default as prop }        from './prop'
export { default as propEq }      from './propEq'
export { default as reduce }      from './reduce'
export { default as foldl }       from './reduce'
export { default as reduceRight } from './reduceRight'
export { default as foldr }       from './reduceRight'
export { default as reject }      from './reject'
export { default as reverse }     from './reverse'
export { default as tail }        from './tail'
export { default as take }        from './take'
export { default as takeUntil }   from './takeUntil'
export { default as toLower }     from './toLower'
export { default as toUpper }     from './toUpper'
export { default as trace }       from './trace'
export { default as unzipObj }    from './unzipObj'
export { default as zip }         from './zip'
export { default as zipObj }      from './zipObj'
<commit_msg>feat(assoc): add alias for merge<commit_after>export { default as add }         from './add'
export { default as assoc }       from './merge'
export { default as compose }     from './compose'
export { default as curry }       from './curry'
export { default as curryN }      from './curryN'
export { default as filter }      from './filter'
export { default as findIndex }   from './findIndex'
export { default as flatMap }     from './flatMap'
export { default as flatten }     from './flatten'
export { default as forEach }     from './forEach'
export { default as head }        from './head'
export { default as indexOf }     from './indexOf'
export { default as keys }        from './keys'
export { default as last }        from './last'
export { default as map }         from './map'
export { default as merge }       from './merge'
export { default as pipe }        from './pipe'
export { default as prop }        from './prop'
export { default as propEq }      from './propEq'
export { default as reduce }      from './reduce'
export { default as foldl }       from './reduce'
export { default as reduceRight } from './reduceRight'
export { default as foldr }       from './reduceRight'
export { default as reject }      from './reject'
export { default as reverse }     from './reverse'
export { default as tail }        from './tail'
export { default as take }        from './take'
export { default as takeUntil }   from './takeUntil'
export { default as toLower }     from './toLower'
export { default as toUpper }     from './toUpper'
export { default as trace }       from './trace'
export { default as unzipObj }    from './unzipObj'
export { default as zip }         from './zip'
export { default as zipObj }      from './zipObj'
<|endoftext|>


<commit_before>Kalendae.Input = function (targetElement, options) {
	if (typeof document.addEventListener !== 'function'  && !util.isIE8()) return;

	var $input = this.input = util.$(targetElement),
		overwriteInput,
		$closeButton,
		changing = false;

	if (!$input || $input.tagName !== 'INPUT') throw "First argument for Kalendae.Input must be an <input> element or a valid element id.";

	var self = this,
		classes = self.classes,
		opts = self.settings = util.merge(self.defaults, options);

	this._events = {};

	//force attachment to the body
	opts.attachTo = window.document.body;

	//if no override provided, use the input's contents
	if (!opts.selected) opts.selected = $input.value;
	else overwriteInput = true;

	//call our parent constructor
	Kalendae.call(self, opts);

	//create the close button
	if (opts.closeButton) {
		$closeButton = util.make('a', {'class':classes.closeButton}, self.container);
		util.addEvent($closeButton, 'click', function () {
			$input.blur();
		});
	}

	if (overwriteInput) $input.value = self.getSelected();

	var $container = self.container,
		noclose = false;

	$container.style.display = 'none';
	util.addClassName($container, classes.positioned);

	this._events.containerMouseDown = util.addEvent($container, 'mousedown', function (event, target) {
		noclose = true; //IE8 doesn't obey event blocking when it comes to focusing, so we have to do this shit.
	});

	this._events.documentMousedown = util.addEvent(window.document, 'mousedown', function (event, target) {
		noclose = false;
	});

	this._events.inputFocus = util.addEvent($input, 'focus', function () {
		changing = true; // prevent setSelected from altering the input contents.
		self.setSelected(this.value);
		changing = false;
		self.show();
	});

	this._events.inputBlur = util.addEvent($input, 'blur', function () {
		if (noclose && util.isIE8()) {
			noclose = false;
			$input.focus();
		}
		else self.hide();
	});

	this._events.inputKeyup = util.addEvent($input, 'keyup', function (event) {
		changing = true; // prevent setSelected from altering the input contents.
		self.setSelected(this.value);
		changing = false;
	});

	var $scrollContainer = util.scrollContainer($input);

	if( $scrollContainer ) {

		// Hide calendar when $scrollContainer is scrolled
		util.addEvent($scrollContainer, 'scroll', function (event) {
			$input.blur();
		});
	}

	self.subscribe('change', function () {
		if (changing) {
			// the change event came from an internal modification, don't update the field contents
			return;
		}
		$input.value = self.getSelected();
		util.fireEvent($input, 'change');
	});

};

Kalendae.Input.prototype = util.merge(Kalendae.prototype, {
	defaults : util.merge(Kalendae.prototype.defaults, {
		format: 'MM/DD/YYYY',
		side: 'bottom',
		closeButton: true,
		offsetLeft: 0,
		offsetTop: 0
	}),
	classes : util.merge(Kalendae.prototype.classes, {
		positioned : 'k-floating',
		closeButton: 'k-btn-close'
	}),

	show : function () {
		var $container = this.container,
			style = $container.style,
			$input = this.input,
			pos = util.getPosition($input),
			$scrollContainer = util.scrollContainer($input),
			scrollTop = $scrollContainer ? $scrollContainer.scrollTop : 0,
			opts = this.settings;

		style.display = '';
		switch (opts.side) {
			case 'left':
				style.left = (pos.left - util.getWidth($container) + opts.offsetLeft) + 'px';
				style.top  = (pos.top + opts.offsetTop - scrollTop) + 'px';
				break;
			case 'right':
				style.left = (pos.left + util.getWidth($input)) + 'px';
				style.top  = (pos.top + opts.offsetTop - scrollTop) + 'px';
				break;
			case 'top':
				style.left = (pos.left + opts.offsetLeft) + 'px';
				style.top  = (pos.top - util.getHeight($container) + opts.offsetTop - scrollTop) + 'px';
				break;
			case 'bottom':
				/* falls through */
			default:
				style.left = (pos.left + opts.offsetLeft) + 'px';
				style.top  = (pos.top + util.getHeight($input) + opts.offsetTop - scrollTop) + 'px';
				break;
		}

		style.position = util.isFixed($input) ? 'fixed' : 'absolute';

		this.publish('show', this);
	},

	hide : function () {
		this.container.style.display = 'none';
		this.publish('hide', this);
	},

	destroy : function() {
		var $container = this.container;
		var $input = this.input;

		util.removeEvent($container, 'mousedown', this._events.containerMousedown);

		util.removeEvent(window.document, 'mousedown', this._events.documentMousedown);

		util.removeEvent($input, 'focus', this._events.inputFocus);

		util.removeEvent($input, 'blur', this._events.inputBlur);

		util.removeEvent($input, 'keyup', this._events.inputKeyup);

		$container.remove();
	}
});

<commit_msg>added 'bottom right' side<commit_after>Kalendae.Input = function (targetElement, options) {
	if (typeof document.addEventListener !== 'function'  && !util.isIE8()) return;

	var $input = this.input = util.$(targetElement),
		overwriteInput,
		$closeButton,
		changing = false;

	if (!$input || $input.tagName !== 'INPUT') throw "First argument for Kalendae.Input must be an <input> element or a valid element id.";

	var self = this,
		classes = self.classes,
		opts = self.settings = util.merge(self.defaults, options);

	this._events = {};

	//force attachment to the body
	opts.attachTo = window.document.body;

	//if no override provided, use the input's contents
	if (!opts.selected) opts.selected = $input.value;
	else overwriteInput = true;

	//call our parent constructor
	Kalendae.call(self, opts);

	//create the close button
	if (opts.closeButton) {
		$closeButton = util.make('a', {'class':classes.closeButton}, self.container);
		util.addEvent($closeButton, 'click', function () {
			$input.blur();
		});
	}

	if (overwriteInput) $input.value = self.getSelected();

	var $container = self.container,
		noclose = false;

	$container.style.display = 'none';
	util.addClassName($container, classes.positioned);

	this._events.containerMouseDown = util.addEvent($container, 'mousedown', function (event, target) {
		noclose = true; //IE8 doesn't obey event blocking when it comes to focusing, so we have to do this shit.
	});

	this._events.documentMousedown = util.addEvent(window.document, 'mousedown', function (event, target) {
		noclose = false;
	});

	this._events.inputFocus = util.addEvent($input, 'focus', function () {
		changing = true; // prevent setSelected from altering the input contents.
		self.setSelected(this.value);
		changing = false;
		self.show();
	});

	this._events.inputBlur = util.addEvent($input, 'blur', function () {
		if (noclose && util.isIE8()) {
			noclose = false;
			$input.focus();
		}
		else self.hide();
	});

	this._events.inputKeyup = util.addEvent($input, 'keyup', function (event) {
		changing = true; // prevent setSelected from altering the input contents.
		self.setSelected(this.value);
		changing = false;
	});

	var $scrollContainer = util.scrollContainer($input);

	if( $scrollContainer ) {

		// Hide calendar when $scrollContainer is scrolled
		util.addEvent($scrollContainer, 'scroll', function (event) {
			$input.blur();
		});
	}

	self.subscribe('change', function () {
		if (changing) {
			// the change event came from an internal modification, don't update the field contents
			return;
		}
		$input.value = self.getSelected();
		util.fireEvent($input, 'change');
	});

};

Kalendae.Input.prototype = util.merge(Kalendae.prototype, {
	defaults : util.merge(Kalendae.prototype.defaults, {
		format: 'MM/DD/YYYY',
		side: 'bottom',
		closeButton: true,
		offsetLeft: 0,
		offsetTop: 0
	}),
	classes : util.merge(Kalendae.prototype.classes, {
		positioned : 'k-floating',
		closeButton: 'k-btn-close'
	}),

	show : function () {
		var $container = this.container,
			style = $container.style,
			$input = this.input,
			pos = util.getPosition($input),
			$scrollContainer = util.scrollContainer($input),
			scrollTop = $scrollContainer ? $scrollContainer.scrollTop : 0,
			opts = this.settings;

		style.display = '';
		switch (opts.side) {
			case 'left':
				style.left = (pos.left - util.getWidth($container) + opts.offsetLeft) + 'px';
				style.top  = (pos.top + opts.offsetTop - scrollTop) + 'px';
				break;
			case 'right':
				style.left = (pos.left + util.getWidth($input)) + 'px';
				style.top  = (pos.top + opts.offsetTop - scrollTop) + 'px';
				break;
			case 'top':
				style.left = (pos.left + opts.offsetLeft) + 'px';
				style.top  = (pos.top - util.getHeight($container) + opts.offsetTop - scrollTop) + 'px';
				break;
			case 'bottom right':
				style.left = (pos.left - util.getWidth($container) + util.getWidth($input) + opts.offsetLeft) + 'px';
				style.top  = (pos.top + util.getHeight($input) + opts.offsetTop - scrollTop) + 'px';
				break;
			case 'bottom':
				/* falls through */
			default:
				style.left = (pos.left + opts.offsetLeft) + 'px';
				style.top  = (pos.top + util.getHeight($input) + opts.offsetTop - scrollTop) + 'px';
				break;
		}

		style.position = util.isFixed($input) ? 'fixed' : 'absolute';

		this.publish('show', this);
	},

	hide : function () {
		this.container.style.display = 'none';
		this.publish('hide', this);
	},

	destroy : function() {
		var $container = this.container;
		var $input = this.input;

		util.removeEvent($container, 'mousedown', this._events.containerMousedown);

		util.removeEvent(window.document, 'mousedown', this._events.documentMousedown);

		util.removeEvent($input, 'focus', this._events.inputFocus);

		util.removeEvent($input, 'blur', this._events.inputBlur);

		util.removeEvent($input, 'keyup', this._events.inputKeyup);

		$container.remove();
	}
});

<|endoftext|>


<commit_before>console.log('Flow Headless Test Successful');
phantom.exit();<commit_msg>improve headless-test<commit_after>var webpage = require('webpage');
var system = require('system');

var opts = parseOpts(system.args.slice(1));
console.log('opts', JSON.stringify(opts));
console.log('Flow Headless Test Successful');
phantom.exit();

function printUsageAndExit(message) {
  console.log("*** " + message + " ***");
  console.log('Usage: phantomjs headless-test.js [--host ip:port] [--timeout seconds] [--packs foo:bar:baz] [--perf date buildId gitHash gitBranch ncpu os jobName outputDir] [--excludeFlows flow1;flow2]');
  console.log('    ip:port      defaults to localhost:54321');
  console.log('    timeout      defaults to 3600');
  console.log('    packs        defaults to examples');
  console.log('    perf         performance of individual tests will be recorded in perf.csv in the output directory');
  console.log('    excludeFlows do not run these flows');
  return phantom.exit(1);
};

function parseOpts(args) {
  var i, opts;
  console.log("Using args " + (args.join(' ')));
  i = 0;
  opts = {};
  while (i < args.length) {
    if (args[i] === "--host") {
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['hostname'] = args[i];
    } else if (args[i] === "--timeout") {
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['timeout'] = args[i];
    } else if (args[i] === "--packs") {
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['packs'] = args[i];
    } else if (args[i] === "--perf") {
      opts['perf'] = true;
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['date'] = args[i];
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['buildId'] = args[i];
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['gitHash'] = args[i];
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['gitBranch'] = args[i];
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['ncpu'] = args[i];
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['os'] = args[i];
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['jobName'] = args[i];
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['outputDir'] = args[i];
    } else if (args[i] === "--excludeFlows") {
      i = i + 1;
      if (i > args.length) {
        printUsageAndExit("Unknown argument: " + args[i]);
      }
      opts['excludeFlows'] = args[i];
    } else {
      printUsageAndExit("Unknown argument: " + args[i]);
    }
    i = i + 1;
  }
  return opts;
};
<|endoftext|>


<commit_before>var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;

var SUPPORT_TOUCH = ('ontouchstart' in window);
var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);

var INPUT_TYPE_TOUCH = 'touch';
var INPUT_TYPE_PEN = 'pen';
var INPUT_TYPE_MOUSE = 'mouse';
var INPUT_TYPE_KINECT = 'kinect';

var COMPUTE_INTERVAL = 25;

var INPUT_START = 1;
var INPUT_MOVE = 2;
var INPUT_END = 4;
var INPUT_CANCEL = 8;

var DIRECTION_NONE = 1;
var DIRECTION_LEFT = 2;
var DIRECTION_RIGHT = 4;
var DIRECTION_UP = 8;
var DIRECTION_DOWN = 16;

var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;

var PROPS_XY = ['x', 'y'];
var PROPS_CLIENT_XY = ['clientX', 'clientY'];

/**
 * create new input type manager
 * @param {Manager} manager
 * @param {Function} callback
 * @returns {Input}
 * @constructor
 */
function Input(manager, callback) {
    var self = this;
    this.manager = manager;
    this.callback = callback;

    // smaller wrapper around the handler, for the scope and the enabled state of the manager,
    // so when disabled the input events are completely bypassed.
    this.domHandler = function(ev) {
        if (boolOrFn(self.manager.options.enable, [self.manager])) {
            self.handler(ev);
        }
    };

    this.evEl && addEventListeners(this.manager.element, this.evEl, this.domHandler);
    this.evWin && addEventListeners(window, this.evWin, this.domHandler);
}

Input.prototype = {
    /**
     * should handle the inputEvent data and trigger the callback
     * @virtual
     */
    handler: function() { },

    /**
     * unbind the events
     */
    destroy: function() {
        this.elEvents && removeEventListeners(this.manager.element, this.elEvents, this.domHandler);
        this.winEvents && removeEventListeners(window, this.winEvents, this.domHandler);
    }
};

/**
 * create new input type manager
 * @param {Hammer} manager
 * @returns {Input}
 */
function createInputInstance(manager) {
    var Type;
    if (SUPPORT_POINTER_EVENTS) {
        Type = PointerEventInput;
    } else if (SUPPORT_ONLY_TOUCH) {
        Type = TouchInput;
    } else if (!SUPPORT_TOUCH) {
        Type = MouseInput;
    } else {
        Type = TouchMouseInput;
    }
    return new (Type)(manager, inputHandler);
}

/**
 * handle input events
 * @param {Manager} manager
 * @param {String} eventType
 * @param {Object} input
 */
function inputHandler(manager, eventType, input) {

    var pointersLen = input.pointers.length;
    var changedPointersLen = input.changedPointers.length;
    var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
    var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));

    input.isFirst = isFirst;
    input.isFinal = isFinal;

    if (isFirst) {
        manager.session = {};
    }
    // source event is the normalized value of the domEvents
    // like 'touchstart, mouseup, pointerdown'
    input.eventType = eventType;

    // compute scale, rotation etc
    computeInputData(manager, input);

    manager.emit('hammer.input', input);

    manager.recognize(input);
}

/**
 * extend the data with some usable properties like scale, rotate, velocity etc
 * @param {Object} manager
 * @param {Object} input
 */
function computeInputData(manager, input) {
    var session = manager.session;
    var pointers = input.pointers;
    var pointersLength = pointers.length;

    // store the first input to calculate the distance and direction
    if (!session.firstInput) {
        session.firstInput = simpleCloneInputData(input);
    }

    // to compute scale and rotation we need to store the multiple touches
    if (pointersLength > 1 && !session.firstMultiple) {
        session.firstMultiple = simpleCloneInputData(input);
    } else if (pointersLength === 1) {
        session.firstMultiple = false;
    }

    var firstInput = session.firstInput;
    var firstMultiple = session.firstMultiple;
    var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
    var center = getCenter(pointers);

    input.timeStamp = now();
    input.deltaTime = input.timeStamp - firstInput.timeStamp;
    input.deltaX = center.x - offsetCenter.x;
    input.deltaY = center.y - offsetCenter.y;

    input.center = center;
    input.angle = getAngle(offsetCenter, center);
    input.distance = getDistance(offsetCenter, center);
    input.offsetDirection = getDirection(input.deltaX, input.deltaY);

    input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
    input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;

    // find the correct target
    var target = manager.element;
    if (hasParent(input.srcEvent.target, target)) {
        target = input.srcEvent.target;
    }
    input.target = target;

    computeIntervalInputData(session, input);
}

/**
 * velocity is calculated every x ms
 * @param {Object} session
 * @param {Object} input
 */
function computeIntervalInputData(session, input) {
    var last = session.lastInterval;
    if (!last) {
        last = session.lastInterval = simpleCloneInputData(input);
    }

    var deltaTime = input.timeStamp - last.timeStamp,
        velocity,
        velocityX,
        velocityY,
        direction;

    if (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined) {
        var deltaX = last.deltaX - input.deltaX;
        var deltaY = last.deltaY - input.deltaY;

        var v = getVelocity(deltaTime, deltaX, deltaY);
        velocityX = v.x;
        velocityY = v.y;
        velocity = Math.max(v.x, v.y);
        direction = getDirection(deltaX, deltaY);
    } else {
        // use latest velocity info if it doesn't overtake a minimum period
        velocity = last.velocity;
        velocityX = last.velocityX;
        velocityY = last.velocityY;
        direction = last.direction;
    }

    input.velocity = velocity;
    input.velocityX = velocityX;
    input.velocityY = velocityY;
    input.direction = direction;
}

/**
 * create a simple clone from the input used for storage of firstInput and firstMultiple
 * @param {Object} input
 * @returns {Object} clonedInputData
 */
function simpleCloneInputData(input) {
    // make a simple copy of the pointers because we will get a reference if we don't
    // we only need clientXY for the calculations
    var pointers = [];
    for (var i = 0; i < input.pointers.length; i++) {
        pointers[i] = {
            clientX: round(input.pointers[i].clientX),
            clientY: round(input.pointers[i].clientY)
        };
    }

    return {
        timeStamp: now(),
        pointers: pointers,
        center: getCenter(pointers),
        deltaX: input.deltaX,
        deltaY: input.deltaY
    };
}

/**
 * get the center of all the pointers
 * @param {Array} pointers
 * @return {Object} center contains `x` and `y` properties
 */
function getCenter(pointers) {
    var pointersLength = pointers.length;

    // no need to loop when only one touch
    if (pointersLength === 1) {
        return {
            x: round(pointers[0].clientX),
            y: round(pointers[0].clientY)
        };
    }

    var x = 0, y = 0;
    for (var i = 0; i < pointersLength; i++) {
        x += pointers[i].clientX;
        y += pointers[i].clientY;
    }

    return {
        x: round(x / pointersLength),
        y: round(y / pointersLength)
    };
}

/**
 * calculate the velocity between two points. unit is in px per ms.
 * @param {Number} deltaTime
 * @param {Number} x
 * @param {Number} y
 * @return {Object} velocity `x` and `y`
 */
function getVelocity(deltaTime, x, y) {
    return {
        x: Math.abs(x / deltaTime) || 0,
        y: Math.abs(y / deltaTime) || 0
    };
}

/**
 * get the direction between two points
 * @param {Number} x
 * @param {Number} y
 * @return {Number} direction
 */
function getDirection(x, y) {
    if (x === y) {
        return DIRECTION_NONE;
    }

    if (Math.abs(x) >= Math.abs(y)) {
        return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
    }
    return y > 0 ? DIRECTION_UP : DIRECTION_DOWN;
}

/**
 * calculate the absolute distance between two points
 * @param {Object} p1 {x, y}
 * @param {Object} p2 {x, y}
 * @param {Array} [props] containing x and y keys
 * @return {Number} distance
 */
function getDistance(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];

    return Math.sqrt((x * x) + (y * y));
}

/**
 * calculate the angle between two coordinates
 * @param {Object} p1
 * @param {Object} p2
 * @param {Array} [props] containing x and y keys
 * @return {Number} angle
 */
function getAngle(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];
    return Math.atan2(y, x) * 180 / Math.PI;
}

/**
 * calculate the rotation degrees between two pointersets
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} rotation
 */
function getRotation(start, end) {
    return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY);
}

/**
 * calculate the scale factor between two pointersets
 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} scale
 */
function getScale(start, end) {
    return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
}
<commit_msg>comment<commit_after>var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;

var SUPPORT_TOUCH = ('ontouchstart' in window);
var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);

var INPUT_TYPE_TOUCH = 'touch';
var INPUT_TYPE_PEN = 'pen';
var INPUT_TYPE_MOUSE = 'mouse';
var INPUT_TYPE_KINECT = 'kinect';

var COMPUTE_INTERVAL = 25;

var INPUT_START = 1;
var INPUT_MOVE = 2;
var INPUT_END = 4;
var INPUT_CANCEL = 8;

var DIRECTION_NONE = 1;
var DIRECTION_LEFT = 2;
var DIRECTION_RIGHT = 4;
var DIRECTION_UP = 8;
var DIRECTION_DOWN = 16;

var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;

var PROPS_XY = ['x', 'y'];
var PROPS_CLIENT_XY = ['clientX', 'clientY'];

/**
 * create new input type manager
 * @param {Manager} manager
 * @param {Function} callback
 * @returns {Input}
 * @constructor
 */
function Input(manager, callback) {
    var self = this;
    this.manager = manager;
    this.callback = callback;

    // smaller wrapper around the handler, for the scope and the enabled state of the manager,
    // so when disabled the input events are completely bypassed.
    this.domHandler = function(ev) {
        if (boolOrFn(self.manager.options.enable, [self.manager])) {
            self.handler(ev);
        }
    };

    this.evEl && addEventListeners(this.manager.element, this.evEl, this.domHandler);
    this.evWin && addEventListeners(window, this.evWin, this.domHandler);
}

Input.prototype = {
    /**
     * should handle the inputEvent data and trigger the callback
     * @virtual
     */
    handler: function() { },

    /**
     * unbind the events
     */
    destroy: function() {
        this.elEvents && removeEventListeners(this.manager.element, this.elEvents, this.domHandler);
        this.winEvents && removeEventListeners(window, this.winEvents, this.domHandler);
    }
};

/**
 * create new input type manager
 * @param {Hammer} manager
 * @returns {Input}
 */
function createInputInstance(manager) {
    var Type;
    if (SUPPORT_POINTER_EVENTS) {
        Type = PointerEventInput;
    } else if (SUPPORT_ONLY_TOUCH) {
        Type = TouchInput;
    } else if (!SUPPORT_TOUCH) {
        Type = MouseInput;
    } else {
        Type = TouchMouseInput;
    }
    return new (Type)(manager, inputHandler);
}

/**
 * handle input events
 * @param {Manager} manager
 * @param {String} eventType
 * @param {Object} input
 */
function inputHandler(manager, eventType, input) {
    var pointersLen = input.pointers.length;
    var changedPointersLen = input.changedPointers.length;
    var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
    var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));

    input.isFirst = isFirst;
    input.isFinal = isFinal;

    if (isFirst) {
        manager.session = {};
    }
    // source event is the normalized value of the domEvents
    // like 'touchstart, mouseup, pointerdown'
    input.eventType = eventType;

    // compute scale, rotation etc
    computeInputData(manager, input);

    // emit secret event
    manager.emit('hammer.input', input);

    manager.recognize(input);
}

/**
 * extend the data with some usable properties like scale, rotate, velocity etc
 * @param {Object} manager
 * @param {Object} input
 */
function computeInputData(manager, input) {
    var session = manager.session;
    var pointers = input.pointers;
    var pointersLength = pointers.length;

    // store the first input to calculate the distance and direction
    if (!session.firstInput) {
        session.firstInput = simpleCloneInputData(input);
    }

    // to compute scale and rotation we need to store the multiple touches
    if (pointersLength > 1 && !session.firstMultiple) {
        session.firstMultiple = simpleCloneInputData(input);
    } else if (pointersLength === 1) {
        session.firstMultiple = false;
    }

    var firstInput = session.firstInput;
    var firstMultiple = session.firstMultiple;
    var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
    var center = getCenter(pointers);

    input.timeStamp = now();
    input.deltaTime = input.timeStamp - firstInput.timeStamp;
    input.deltaX = center.x - offsetCenter.x;
    input.deltaY = center.y - offsetCenter.y;

    input.center = center;
    input.angle = getAngle(offsetCenter, center);
    input.distance = getDistance(offsetCenter, center);
    input.offsetDirection = getDirection(input.deltaX, input.deltaY);

    input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
    input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;

    // find the correct target
    var target = manager.element;
    if (hasParent(input.srcEvent.target, target)) {
        target = input.srcEvent.target;
    }
    input.target = target;

    computeIntervalInputData(session, input);
}

/**
 * velocity is calculated every x ms
 * @param {Object} session
 * @param {Object} input
 */
function computeIntervalInputData(session, input) {
    var last = session.lastInterval;
    if (!last) {
        last = session.lastInterval = simpleCloneInputData(input);
    }

    var deltaTime = input.timeStamp - last.timeStamp,
        velocity,
        velocityX,
        velocityY,
        direction;

    if (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined) {
        var deltaX = last.deltaX - input.deltaX;
        var deltaY = last.deltaY - input.deltaY;

        var v = getVelocity(deltaTime, deltaX, deltaY);
        velocityX = v.x;
        velocityY = v.y;
        velocity = Math.max(v.x, v.y);
        direction = getDirection(deltaX, deltaY);
    } else {
        // use latest velocity info if it doesn't overtake a minimum period
        velocity = last.velocity;
        velocityX = last.velocityX;
        velocityY = last.velocityY;
        direction = last.direction;
    }

    input.velocity = velocity;
    input.velocityX = velocityX;
    input.velocityY = velocityY;
    input.direction = direction;
}

/**
 * create a simple clone from the input used for storage of firstInput and firstMultiple
 * @param {Object} input
 * @returns {Object} clonedInputData
 */
function simpleCloneInputData(input) {
    // make a simple copy of the pointers because we will get a reference if we don't
    // we only need clientXY for the calculations
    var pointers = [];
    for (var i = 0; i < input.pointers.length; i++) {
        pointers[i] = {
            clientX: round(input.pointers[i].clientX),
            clientY: round(input.pointers[i].clientY)
        };
    }

    return {
        timeStamp: now(),
        pointers: pointers,
        center: getCenter(pointers),
        deltaX: input.deltaX,
        deltaY: input.deltaY
    };
}

/**
 * get the center of all the pointers
 * @param {Array} pointers
 * @return {Object} center contains `x` and `y` properties
 */
function getCenter(pointers) {
    var pointersLength = pointers.length;

    // no need to loop when only one touch
    if (pointersLength === 1) {
        return {
            x: round(pointers[0].clientX),
            y: round(pointers[0].clientY)
        };
    }

    var x = 0, y = 0;
    for (var i = 0; i < pointersLength; i++) {
        x += pointers[i].clientX;
        y += pointers[i].clientY;
    }

    return {
        x: round(x / pointersLength),
        y: round(y / pointersLength)
    };
}

/**
 * calculate the velocity between two points. unit is in px per ms.
 * @param {Number} deltaTime
 * @param {Number} x
 * @param {Number} y
 * @return {Object} velocity `x` and `y`
 */
function getVelocity(deltaTime, x, y) {
    return {
        x: Math.abs(x / deltaTime) || 0,
        y: Math.abs(y / deltaTime) || 0
    };
}

/**
 * get the direction between two points
 * @param {Number} x
 * @param {Number} y
 * @return {Number} direction
 */
function getDirection(x, y) {
    if (x === y) {
        return DIRECTION_NONE;
    }

    if (Math.abs(x) >= Math.abs(y)) {
        return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
    }
    return y > 0 ? DIRECTION_UP : DIRECTION_DOWN;
}

/**
 * calculate the absolute distance between two points
 * @param {Object} p1 {x, y}
 * @param {Object} p2 {x, y}
 * @param {Array} [props] containing x and y keys
 * @return {Number} distance
 */
function getDistance(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];

    return Math.sqrt((x * x) + (y * y));
}

/**
 * calculate the angle between two coordinates
 * @param {Object} p1
 * @param {Object} p2
 * @param {Array} [props] containing x and y keys
 * @return {Number} angle
 */
function getAngle(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];
    return Math.atan2(y, x) * 180 / Math.PI;
}

/**
 * calculate the rotation degrees between two pointersets
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} rotation
 */
function getRotation(start, end) {
    return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY);
}

/**
 * calculate the scale factor between two pointersets
 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} scale
 */
function getScale(start, end) {
    return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
}
<|endoftext|>


<commit_before>Minionette.Region = function(options) {
    // Setup a unique id for this region.
    // Not really necessary, but it doesn't hurt.
    this.cid = _.uniqueId('subview');

    // Make sure we have a view.
    this._ensureView(options || {});
};

// Allow Regions to be extended.
// Backbone's extend is generic, just copy it over.
Minionette.Region.extend = Backbone.View.extend;

_.extend(Minionette.Region.prototype, Backbone.Events, {
    // The place holder view's class
    View: Minionette.View.extend({
        // Set it to a span, so when it's empty it's
        // collapsed on the DOM.
        tagName: 'span',
        // Use the data-cid attribute as a unique
        // attribute. Used for reattaching a detached view.
        attributes: function() {
            return {'data-cid': this.cid};
        }
    }),

    // Ensures the region has a view.
    _ensureView: function(options) {
        // Instantiate our place holder view.
        this._view = new this.View();

        // Set this.view to instantiated options.view
        // if supplied.
        if (options.view) { this.view = options.view; }

        // Make sure the view is an instance of Backbone.View
        // (or a subclass). If not, set the view to our place holder
        // view.
        if (!(this.view instanceof Backbone.View)) {
            this.view = this._view;
        }

        // And set our view's _parent to this region.
        this._assignParent(this.view);
    },

    // Resets the region's view to place holder view.
    // Optionally takes a boolean, in which case the
    // oldView will just be detached.
    reset: function(detach) {
        this.attach(this._view, detach);
    },

    // A proxy method to the view's render().
    render: function() {
        return this.view.render();
    },

    // Attaches newView. This sets newView#$el
    // at the same index (inside the parent element)
    // as the old view, and removes the old view.
    attach: function(newView, detach) {
        this._assignParent(newView);
        var oldView = this.view;

        // Places newView after the current view.
        this.view.$el.after(newView.$el);
        // And detaches the view.
        this.view.$el.detach();

        this.view = newView;

        // Remove the view
        if (!detach) {
            oldView.remove();
        }

        return newView;
    },

    // Removes this region, and it's view.
    remove: function() {
        this._removeViews();
        this._removeFromParent();
    },

    _removeViews: function() {
        this.view.remove();
        this._view.remove();
        this._detachedView && this._detachedView.remove();
    },

    _removeFromParent: function() {
        if (this._parent && this._parent._removeRegion) {
            this._parent._removeRegion(this);
        }
        this._parent = null;
    },

    // Detaches the current view, replacing it with
    // the place holder. Convenient for detaching
    // during rendering.
    detach: function() {
        // Store the current view for later reattaching.
        this._detachedView = this.view;

        if (this.view !== this._view) {
            this.reset(true);
        }

        return this;
    },

    // Reattaches the detached view.
    reattach: function() {
        if (!this._detachedView) { return; }

        // $context is a scoped context in which to search
        // for the current view's element in.
        var $context = (this._parent && this._parent.$el) || Backbone.$(document.body),
            viewSelector = '[data-cid=' + this.view.cid + ']',
            newView = this._detachedView;

        // We then replace the current view with the detached view.
        $context.find(viewSelector).replaceWith(newView.$el);

        delete this._detachedView;
        this.view = newView;

        return newView;
    },

    // A hook method that is called during
    // a view#remove(). Allows a view to be removed,
    // replacing it with the place holder.
    _removeView: function(view) {
        if (this.view === view) {
            this.reset(true);
        }
    },

    // Sets view#_parent to this region.
    // So view#remove() can hook into our
    // _removeView().
    _assignParent: function(view) {
        view._parent = this;
    }
});
<commit_msg>Rename a Region's cid to have region instead of subview<commit_after>Minionette.Region = function(options) {
    // Setup a unique id for this region.
    // Not really necessary, but it doesn't hurt.
    this.cid = _.uniqueId('region');

    // Make sure we have a view.
    this._ensureView(options || {});
};

// Allow Regions to be extended.
// Backbone's extend is generic, just copy it over.
Minionette.Region.extend = Backbone.View.extend;

_.extend(Minionette.Region.prototype, Backbone.Events, {
    // The place holder view's class
    View: Minionette.View.extend({
        // Set it to a span, so when it's empty it's
        // collapsed on the DOM.
        tagName: 'span',
        // Use the data-cid attribute as a unique
        // attribute. Used for reattaching a detached view.
        attributes: function() {
            return {'data-cid': this.cid};
        }
    }),

    // Ensures the region has a view.
    _ensureView: function(options) {
        // Instantiate our place holder view.
        this._view = new this.View();

        // Set this.view to instantiated options.view
        // if supplied.
        if (options.view) { this.view = options.view; }

        // Make sure the view is an instance of Backbone.View
        // (or a subclass). If not, set the view to our place holder
        // view.
        if (!(this.view instanceof Backbone.View)) {
            this.view = this._view;
        }

        // And set our view's _parent to this region.
        this._assignParent(this.view);
    },

    // Resets the region's view to place holder view.
    // Optionally takes a boolean, in which case the
    // oldView will just be detached.
    reset: function(detach) {
        this.attach(this._view, detach);
    },

    // A proxy method to the view's render().
    render: function() {
        return this.view.render();
    },

    // Attaches newView. This sets newView#$el
    // at the same index (inside the parent element)
    // as the old view, and removes the old view.
    attach: function(newView, detach) {
        this._assignParent(newView);
        var oldView = this.view;

        // Places newView after the current view.
        this.view.$el.after(newView.$el);
        // And detaches the view.
        this.view.$el.detach();

        this.view = newView;

        // Remove the view
        if (!detach) {
            oldView.remove();
        }

        return newView;
    },

    // Removes this region, and it's view.
    remove: function() {
        this._removeViews();
        this._removeFromParent();
    },

    _removeViews: function() {
        this.view.remove();
        this._view.remove();
        this._detachedView && this._detachedView.remove();
    },

    _removeFromParent: function() {
        if (this._parent && this._parent._removeRegion) {
            this._parent._removeRegion(this);
        }
        this._parent = null;
    },

    // Detaches the current view, replacing it with
    // the place holder. Convenient for detaching
    // during rendering.
    detach: function() {
        // Store the current view for later reattaching.
        this._detachedView = this.view;

        if (this.view !== this._view) {
            this.reset(true);
        }

        return this;
    },

    // Reattaches the detached view.
    reattach: function() {
        if (!this._detachedView) { return; }

        // $context is a scoped context in which to search
        // for the current view's element in.
        var $context = (this._parent && this._parent.$el) || Backbone.$(document.body),
            viewSelector = '[data-cid=' + this.view.cid + ']',
            newView = this._detachedView;

        // We then replace the current view with the detached view.
        $context.find(viewSelector).replaceWith(newView.$el);

        delete this._detachedView;
        this.view = newView;

        return newView;
    },

    // A hook method that is called during
    // a view#remove(). Allows a view to be removed,
    // replacing it with the place holder.
    _removeView: function(view) {
        if (this.view === view) {
            this.reset(true);
        }
    },

    // Sets view#_parent to this region.
    // So view#remove() can hook into our
    // _removeView().
    _assignParent: function(view) {
        view._parent = this;
    }
});
<|endoftext|>


<commit_before>/* global document */
import 'normalize.css';
import 'babel-polyfill';

import sizeHelper   from '../../../../Common/Misc/SizeHelper';

import CompositeClosureHelper from '../../../../../src/Common/Core/CompositeClosureHelper';
import FieldProvider from '../../../../../src/InfoViz/Core/FieldProvider';
import LegendProvider from '../../../../../src/InfoViz/Core/LegendProvider';
import Histogram1DProvider from '../../../../../src/InfoViz/Core/Histogram1DProvider';
import HistogramBinHoverProvider from '../../../../../src/InfoViz/Core/HistogramBinHoverProvider';
import ScoresProvider from '../../../../../src/InfoViz/Core/ScoresProvider';
import SelectionProvider from '../../../../../src/InfoViz/Core/SelectionProvider';
import AnnotationStoreProvider from '../../../../../src/InfoViz/Core/AnnotationStoreProvider';
import HistogramSelector from '../../../Native/HistogramSelector';
import FieldSelector from '../../../Native/FieldSelector';

import dataModel from './state.json';

const bodyElt = document.querySelector('body');
// '100vh' is 100% of the current screen height
const defaultHeight = '100vh';

const histogramSelectorContainer = document.createElement('div');
histogramSelectorContainer.style.position = 'relative';
histogramSelectorContainer.style.width = '58%';
histogramSelectorContainer.style.height = defaultHeight;
histogramSelectorContainer.style.float = 'left';
bodyElt.appendChild(histogramSelectorContainer);

const fieldSelectorContainer = document.createElement('div');
fieldSelectorContainer.style.position = 'relative';
fieldSelectorContainer.style.width = '42%';
fieldSelectorContainer.style.height = defaultHeight;
fieldSelectorContainer.style.float = 'left';
fieldSelectorContainer.style['font-size'] = '10pt';
bodyElt.appendChild(fieldSelectorContainer);

const provider = CompositeClosureHelper.newInstance((publicAPI, model, initialValues = {}) => {
  Object.assign(model, initialValues);
  FieldProvider.extend(publicAPI, model, initialValues);
  Histogram1DProvider.extend(publicAPI, model, initialValues);
  HistogramBinHoverProvider.extend(publicAPI, model);
  LegendProvider.extend(publicAPI, model, initialValues);
  ScoresProvider.extend(publicAPI, model, initialValues);
  SelectionProvider.extend(publicAPI, model, initialValues);
  AnnotationProvider.extend(publicAPI, model, initialValues);
})(dataModel);

// set provider behaviors
provider.setFieldsSorted(true);
provider.getFieldNames().forEach((name) => {
  provider.addLegendEntry(name);
});
provider.assignLegend(['colors', 'shapes']);

// activate scoring gui
const scores = [
  { name: 'No', color: '#FDAE61', value: -1 },
  { name: 'Maybe', color: '#FFFFBF', value: 0 },
  { name: 'Yes', color: '#A6D96A', value: 1 },
];
provider.setScores(scores);
provider.setDefaultScore(1);

// Create histogram selector
const histogramSelector = HistogramSelector.newInstance({
  provider,
  container: histogramSelectorContainer,
  // defaultScore: 1,
});
// set a target number per row.
histogramSelector.requestNumBoxesPerRow(4);
// Or show a single variable as the focus, possibly disabling switching to other vars.
// histogramSelector.displaySingleHistogram(provider.getFieldNames()[5], true);
// and maybe set a scoring annotation:
// histogramSelector.setDefaultScorePartition(provider.getFieldNames()[5]);
// test reset:
// window.setTimeout(() => {
//   histogramSelector.requestNumBoxesPerRow(4);
// }, 5000);

// Create field selector
const fieldSelector = FieldSelector.newInstance({ provider, container: fieldSelectorContainer });

// Listen to window resize
sizeHelper.onSizeChange(() => {
  histogramSelector.resize();
  fieldSelector.resize();
});
sizeHelper.startListening();

sizeHelper.triggerChange();
<commit_msg>fix(HistogramSelector): example, revert mismatched provider.<commit_after>/* global document */
import 'normalize.css';
import 'babel-polyfill';

import sizeHelper   from '../../../../Common/Misc/SizeHelper';

import CompositeClosureHelper from '../../../../../src/Common/Core/CompositeClosureHelper';
import FieldProvider from '../../../../../src/InfoViz/Core/FieldProvider';
import LegendProvider from '../../../../../src/InfoViz/Core/LegendProvider';
import Histogram1DProvider from '../../../../../src/InfoViz/Core/Histogram1DProvider';
import HistogramBinHoverProvider from '../../../../../src/InfoViz/Core/HistogramBinHoverProvider';
import ScoresProvider from '../../../../../src/InfoViz/Core/ScoresProvider';
import SelectionProvider from '../../../../../src/InfoViz/Core/SelectionProvider';
import HistogramSelector from '../../../Native/HistogramSelector';
import FieldSelector from '../../../Native/FieldSelector';

import dataModel from './state.json';

const bodyElt = document.querySelector('body');
// '100vh' is 100% of the current screen height
const defaultHeight = '100vh';

const histogramSelectorContainer = document.createElement('div');
histogramSelectorContainer.style.position = 'relative';
histogramSelectorContainer.style.width = '58%';
histogramSelectorContainer.style.height = defaultHeight;
histogramSelectorContainer.style.float = 'left';
bodyElt.appendChild(histogramSelectorContainer);

const fieldSelectorContainer = document.createElement('div');
fieldSelectorContainer.style.position = 'relative';
fieldSelectorContainer.style.width = '42%';
fieldSelectorContainer.style.height = defaultHeight;
fieldSelectorContainer.style.float = 'left';
fieldSelectorContainer.style['font-size'] = '10pt';
bodyElt.appendChild(fieldSelectorContainer);

const provider = CompositeClosureHelper.newInstance((publicAPI, model, initialValues = {}) => {
  Object.assign(model, initialValues);
  FieldProvider.extend(publicAPI, model, initialValues);
  Histogram1DProvider.extend(publicAPI, model, initialValues);
  HistogramBinHoverProvider.extend(publicAPI, model);
  LegendProvider.extend(publicAPI, model, initialValues);
  ScoresProvider.extend(publicAPI, model, initialValues);
  SelectionProvider.extend(publicAPI, model, initialValues);
})(dataModel);

// set provider behaviors
provider.setFieldsSorted(true);
provider.getFieldNames().forEach((name) => {
  provider.addLegendEntry(name);
});
provider.assignLegend(['colors', 'shapes']);

// activate scoring gui
const scores = [
  { name: 'No', color: '#FDAE61', value: -1 },
  { name: 'Maybe', color: '#FFFFBF', value: 0 },
  { name: 'Yes', color: '#A6D96A', value: 1 },
];
provider.setScores(scores);
provider.setDefaultScore(1);

// Create histogram selector
const histogramSelector = HistogramSelector.newInstance({
  provider,
  container: histogramSelectorContainer,
  // defaultScore: 1,
});
// set a target number per row.
histogramSelector.requestNumBoxesPerRow(4);
// Or show a single variable as the focus, possibly disabling switching to other vars.
// histogramSelector.displaySingleHistogram(provider.getFieldNames()[5], true);
// and maybe set a scoring annotation:
// histogramSelector.setDefaultScorePartition(provider.getFieldNames()[5]);
// test reset:
// window.setTimeout(() => {
//   histogramSelector.requestNumBoxesPerRow(4);
// }, 5000);

// Create field selector
const fieldSelector = FieldSelector.newInstance({ provider, container: fieldSelectorContainer });

// Listen to window resize
sizeHelper.onSizeChange(() => {
  histogramSelector.resize();
  fieldSelector.resize();
});
sizeHelper.startListening();

sizeHelper.triggerChange();
<|endoftext|>


<commit_before>
import * as modifier from './lib/modifier.js';
import * as word from './lib/word.js';

function datasetSafeDelete(el, ...keys) {
  const d = el.dataset;
  keys.forEach((key) => {
    if (key in d) {
      delete d[key];
    }
  });
}

const upgraded = new WeakMap();

export function cursorPosition(el) {
  const fn = upgraded.get(el);
  if (fn !== undefined) {
    return fn();
  }
  return undefined;
}

// word focus handler
function upgrade(el) {
  if (upgraded.has(el)) {
    return false;
  }

  const helper = document.createElement('div');
  helper.className = 'overflow-helper';
  el.parentNode.insertBefore(helper, el);

  const underline = document.createElement('div');
  underline.className = 'underline';
  helper.appendChild(underline);

  // measures the width of text
  const measureText = (function() {
    const sizer = document.createElement('div');
    sizer.className = 'sizer';
    helper.appendChild(sizer);

    const nonce = document.createElement('div');
    nonce.className = 'nonce';

    return (text) => {
      sizer.textContent = text;
      sizer.appendChild(nonce);
      return nonce.offsetLeft;
    }
  }());

  // record upgraded measurer for callers to find our pixel position
  upgraded.set(el, () => {
    const mid = ~~((el.selectionStart + el.selectionEnd) / 2)
    return measureText(el.value.substr(0, mid)) - el.scrollLeft;
  });

  // hide underline until load: the font used might not be ready, so it's probably out of whack
  if (document.readyState !== 'complete') {
    underline.classList.add('loading');
    window.addEventListener('load', (ev) => {
      renderLine();
      underline.classList.remove('loading');
    });
  }

  const renderLine = () => {
    if (el.dataset.from === undefined) {
      underline.hidden = true;
      return false;
    }
    const [from, to] = [+el.dataset.from, +el.dataset.to];

    // otherwise, record and draw the line
    const left = measureText(el.value.substr(0, from));
    const width = measureText(el.value.substr(from, to - from));

    if (width < 0 && !document.getElementById('less')) {
      // nb. this seems to happen in dev with lesscss
      console.warn('invalid sizer width', width, 'for text', sizer.textContent);
    }

    underline.hidden = width <= 0;
    underline.style.left = left + 'px';
    underline.style.width = width + 'px';
    underline.style.transform = `translateX(-${el.scrollLeft}px)`;
  };

  // force selection
  const setRange = (from, to) => {
    [el.dataset.from, el.dataset.to] = [from, to];
    if (from >= to) {
      datasetSafeDelete(el, 'prefix', 'word', 'focus');
      underline.hidden = true;
      return false;
    }
    el.dataset['focus'] = el.value.substr(from, to - from);
    renderLine();
    return true;
  };

  // state/handler keep track of the current focus word (plus scroll position, if input is big)
  const initialLength = el.value.length;
  const state = {start: initialLength, end: initialLength, value: undefined};
  const changeHandler = (permitNextChange) => {
    if (permitNextChange !== false &&
        el.selectionStart === state.start &&
        el.selectionEnd === state.end &&
        el.value === state.value) {
      return true;  // already at this state
    }
    [state.start, state.end] = [el.selectionStart, el.selectionEnd];
    if (state.value !== el.value) {
      el.dispatchEvent(new CustomEvent('value', {detail: el.value}));
      state.value = el.value;
    }

    // range selection, magic
    if (state.start !== state.end) {
      datasetSafeDelete(el, 'prefix', 'word');

      setRange(state.start, state.end);

      underline.classList.add('range');
      el.classList.add('range');
      return false;
    }
    underline.classList.remove('range');
    el.classList.remove('range');

    // if it's invalid and we were permitted (this is used for faux-highlights), ignore
    const {from, to} = word.match(el.value, state.start);
    if (from >= to && permitNextChange) { return; }
    if (setRange(from, to)) {
      // if the range was valid, update the prefix/focus but delete the word (in typing state)
      el.dataset['focus'] = el.dataset['prefix'] = el.value.substr(from, to - from);
      datasetSafeDelete(el, 'word');
    }
  };

  // runs change handler and emits the 'word' event as appropriate
  let previousDetail = {};
  const mergedEventHandler = (events, permitNextChange) => {
    // if there was a focus event, don't let the browser take over: reset previous known good
    if (events.has('focus')) {
      // only if it wasn't a range selection (otherwise clicking back retains range, weird)
      if (state.start === state.end) {
        // TODO: this sets on initial load, even though it probably doesn't need to
        [el.selectionStart, el.selectionEnd] = [state.start, state.end];
      }
    }

    // run change handler: if true, nothing changed
    if (changeHandler(permitNextChange)) { return; }

    // send query: prefix or whole-word
    let text = el.dataset.prefix || el.dataset.word || null;
    if (!el.dataset.focus) {
      text = '';  // nothing focused, pretend it's empty input
    }
    const detail = {
      text,
      prefix: 'prefix' in el.dataset,
      focus: el.dataset.focus,
      selection: (el.selectionStart !== el.selectionEnd),
    };

    // send event only if something has changed
    if (detail.text !== previousDetail.text ||
        detail.prefix !== previousDetail.prefix ||
        detail.focus !== previousDetail.focus ||
        detail.selection !== previousDetail.selection) {
      previousDetail = detail;
      el.dispatchEvent(new CustomEvent('query', {detail}));
    }
  };

  // dedup listeners on a rAF
  let permitNextChange;  // FIXME: global-ish scope is ugly
  (function() {
    let frame;
    let events = new Set();  // records the events that occured to cause this
    const dedup = (ev) => {
      if (!frame) {
        permitNextChange = undefined;
        events.clear();
        frame = window.requestAnimationFrame(() => {
          frame = null;
          mergedEventHandler(events, permitNextChange);
        });
      }
      ev && events.add(ev.type);
    };

    // lots of listeners for a million different change reasons
    const rest = 'change keydown keypress focus click mousedown select input';
    rest.split(/\s+/).forEach((event) => el.addEventListener(event, dedup));
    dedup();

    // if a user is dragging around, this might be changing the offsetLeft (dragging input l/r)
    el.addEventListener('mousemove', (ev) => {
      if (ev.which) {
        dedup();
      }
    });

    // add 'selectionchange' (only valid on document) to listen to the initial long-press selection
    // on Chrome (possibly others?) mobile: it doesn't generate 'select'.
    document.addEventListener('selectionchange', (ev) => {
      if (document.activeElement === el) {
        dedup();
      }
    });

    // on blur, after a backspace, Chrome moves the start/end selection: fix it
    let scrollLeftOnBlur = false;
    el.addEventListener('blur', (ev) => {
      if (el.scrollLeft) {
        scrollLeftOnBlur = el.scrollLeft || false;
      }
      if (el.selectionStart !== state.start || el.selectionStart !== state.end) {
        [el.selectionStart, el.selectionEnd] = [state.start, state.end];
      }
    }, true);
    el.addEventListener('focusout', (ev) => {
      if (scrollLeftOnBlur !== false) {
        el.scrollLeft = scrollLeftOnBlur;
        scrollLeftOnBlur = false;
      }
    }, true);
  }());

  // add a non-deduped keydown handler, to run before others and intercept space
  el.addEventListener('keydown', (ev) => {
    switch (ev.key) {
    case 'Escape':
      permitNextChange = false;  // force next change
      break;

    case 'ArrowDown':
    case 'Down':
    case 'ArrowUp':
    case 'Up':
      ev.preventDefault();  // disable normal up/down behavior
      return;

    case ' ':
      if (el.dataset.prefix && el.selectionStart === +el.dataset.to) {
        el.dispatchEvent(new CustomEvent('request', {detail: el.dataset.prefix}));
      }

      // TODO: do this to prevent actually space being hit (@samthor prefers it this way)
      //ev.preventDefault();
      break;
    }
  });

  // add a non-deduped keyup handler, for space on mobile browsers
  el.addEventListener('keyup', (ev) => {
    if (ev.keyCode === 229 || !ev.keyCode) {
      // look for a space before whatever was entered.
      const v = el.value.substr(el.selectionStart - 1, 1);
      if (v === ' ' && el.dataset.prefix) {
        el.dispatchEvent(new CustomEvent('request', {detail: el.dataset.prefix}));
      }
    }
  });

  // dedup re-rendering calls
  (function() {
    let frame;
    const dedupRenderLine = () => {
      if (!frame) {
        frame = window.requestAnimationFrame(() => {
          frame = null;
          renderLine();
        });
      }
    };
    window.addEventListener('resize', dedupRenderLine);
    el.addEventListener('wheel', dedupRenderLine, {passive: true});
  }());

  // replace helper
  const replaceFocus = (call) => {
    const previousScrollLeft = el.scrollLeft;
    const [from, to] = [+el.dataset.from, +el.dataset.to];
    const value = el.value.substr(from, to - from);
    let [start, end] = [typer.selectionStart, typer.selectionEnd];

    const update = call(value);
    if (update == null) { return false; }
    typer.value = typer.value.substr(0, from) + update + typer.value.substr(to);

    const drift = (where) => {
      if (where >= to) {
        // after the update
        where = where - (to - from) + update.length;
      } else if (where > from) {
        // during the update
        where = from + update.length;
      } else {
        // do nothing, was before
      }
      return where;
    };

    const prev = document.activeElement;

    typer.focus();
    typer.dispatchEvent(new CustomEvent('change'));  // nb. updates from/to (from won't change)
    // pretend we were like this all along
    [state.start, state.end] = [typer.selectionStart, typer.selectionEnd] = [drift(start), drift(end)];

    prev && prev.focus();

    permitNextChange = true;
    el.scrollLeft = previousScrollLeft;  // before setRange, so the underline is correct
    setRange(from, from + update.length);
    return true;
  };

  // handle 'modifier' event: apply modifiers to the focus word, if any
  el.addEventListener('modifier', (ev) => {
    const arg = {[ev.detail.type]: ev.detail.code};
    replaceFocus((value) => modifier.modify(value, arg).out || '');
  });

  // handle 'emoji' event: if there's a current focus word, then replace it with the new emoji \o/
  el.addEventListener('emoji', (ev) => {
    const emoji = ev.detail.choice;
    if (!replaceFocus(() => emoji)) { return; }

    // listen to the caller's view on what word we should pretend this emoji is
    el.dataset['word'] = ev.detail.word || '';
    datasetSafeDelete(el, 'prefix');
  });
}

upgrade(typer);
<commit_msg>support undo/redo<commit_after>
import * as modifier from './lib/modifier.js';
import * as word from './lib/word.js';

function datasetSafeDelete(el, ...keys) {
  const d = el.dataset;
  keys.forEach((key) => {
    if (key in d) {
      delete d[key];
    }
  });
}

const upgraded = new WeakMap();

export function cursorPosition(el) {
  const fn = upgraded.get(el);
  if (fn !== undefined) {
    return fn();
  }
  return undefined;
}

// word focus handler
function upgrade(el) {
  if (upgraded.has(el)) {
    return false;
  }

  const helper = document.createElement('div');
  helper.className = 'overflow-helper';
  el.parentNode.insertBefore(helper, el);

  const underline = document.createElement('div');
  underline.className = 'underline';
  helper.appendChild(underline);

  // measures the width of text
  const measureText = (function() {
    const sizer = document.createElement('div');
    sizer.className = 'sizer';
    helper.appendChild(sizer);

    const nonce = document.createElement('div');
    nonce.className = 'nonce';

    return (text) => {
      sizer.textContent = text;
      sizer.appendChild(nonce);
      return nonce.offsetLeft;
    }
  }());

  // record upgraded measurer for callers to find our pixel position
  upgraded.set(el, () => {
    const mid = ~~((el.selectionStart + el.selectionEnd) / 2)
    return measureText(el.value.substr(0, mid)) - el.scrollLeft;
  });

  // hide underline until load: the font used might not be ready, so it's probably out of whack
  if (document.readyState !== 'complete') {
    underline.classList.add('loading');
    window.addEventListener('load', (ev) => {
      renderLine();
      underline.classList.remove('loading');
    });
  }

  const renderLine = () => {
    if (el.dataset.from === undefined) {
      underline.hidden = true;
      return false;
    }
    const [from, to] = [+el.dataset.from, +el.dataset.to];

    // otherwise, record and draw the line
    const left = measureText(el.value.substr(0, from));
    const width = measureText(el.value.substr(from, to - from));

    if (width < 0 && !document.getElementById('less')) {
      // nb. this seems to happen in dev with lesscss
      console.warn('invalid sizer width', width, 'for text', sizer.textContent);
    }

    underline.hidden = width <= 0;
    underline.style.left = left + 'px';
    underline.style.width = width + 'px';
    underline.style.transform = `translateX(-${el.scrollLeft}px)`;
  };

  // force selection
  const setRange = (from, to) => {
    [el.dataset.from, el.dataset.to] = [from, to];
    if (from >= to) {
      datasetSafeDelete(el, 'prefix', 'word', 'focus');
      underline.hidden = true;
      return false;
    }
    el.dataset['focus'] = el.value.substr(from, to - from);
    renderLine();
    return true;
  };

  // state/handler keep track of the current focus word (plus scroll position, if input is big)
  const initialLength = el.value.length;
  const state = {start: initialLength, end: initialLength, value: undefined};
  const changeHandler = (permitNextChange) => {
    if (permitNextChange !== false &&
        el.selectionStart === state.start &&
        el.selectionEnd === state.end &&
        el.value === state.value) {
      return true;  // already at this state
    }
    [state.start, state.end] = [el.selectionStart, el.selectionEnd];
    if (state.value !== el.value) {
      el.dispatchEvent(new CustomEvent('value', {detail: el.value}));
      state.value = el.value;
    }

    // range selection, magic
    if (state.start !== state.end) {
      datasetSafeDelete(el, 'prefix', 'word');

      setRange(state.start, state.end);

      underline.classList.add('range');
      el.classList.add('range');
      return false;
    }
    underline.classList.remove('range');
    el.classList.remove('range');

    // if it's invalid and we were permitted (this is used for faux-highlights), ignore
    const {from, to} = word.match(el.value, state.start);
    if (from >= to && permitNextChange) { return; }
    if (setRange(from, to)) {
      // if the range was valid, update the prefix/focus but delete the word (in typing state)
      el.dataset['focus'] = el.dataset['prefix'] = el.value.substr(from, to - from);
      datasetSafeDelete(el, 'word');
    }
  };

  // runs change handler and emits the 'word' event as appropriate
  let previousDetail = {};
  const mergedEventHandler = (events, permitNextChange) => {
    // if there was a focus event, don't let the browser take over: reset previous known good
    if (events.has('focus')) {
      // only if it wasn't a range selection (otherwise clicking back retains range, weird)
      if (state.start === state.end) {
        // TODO: this sets on initial load, even though it probably doesn't need to
        [el.selectionStart, el.selectionEnd] = [state.start, state.end];
      }
    }

    // run change handler: if true, nothing changed
    if (changeHandler(permitNextChange)) { return; }

    // send query: prefix or whole-word
    let text = el.dataset.prefix || el.dataset.word || null;
    if (!el.dataset.focus) {
      text = '';  // nothing focused, pretend it's empty input
    }
    const detail = {
      text,
      prefix: 'prefix' in el.dataset,
      focus: el.dataset.focus,
      selection: (el.selectionStart !== el.selectionEnd),
    };

    // send event only if something has changed
    if (detail.text !== previousDetail.text ||
        detail.prefix !== previousDetail.prefix ||
        detail.focus !== previousDetail.focus ||
        detail.selection !== previousDetail.selection) {
      previousDetail = detail;
      el.dispatchEvent(new CustomEvent('query', {detail}));
    }
  };

  // dedup listeners on a rAF
  let permitNextChange;  // FIXME: global-ish scope is ugly
  (function() {
    let frame;
    let events = new Set();  // records the events that occured to cause this
    const dedup = (ev) => {
      if (!frame) {
        permitNextChange = undefined;
        events.clear();
        frame = window.requestAnimationFrame(() => {
          frame = null;
          mergedEventHandler(events, permitNextChange);
        });
      }
      ev && events.add(ev.type);
    };

    // lots of listeners for a million different change reasons
    const rest = 'change keydown keypress focus click mousedown select input';
    rest.split(/\s+/).forEach((event) => el.addEventListener(event, dedup));
    dedup();

    // if a user is dragging around, this might be changing the offsetLeft (dragging input l/r)
    el.addEventListener('mousemove', (ev) => {
      if (ev.which) {
        dedup();
      }
    });

    // add 'selectionchange' (only valid on document) to listen to the initial long-press selection
    // on Chrome (possibly others?) mobile: it doesn't generate 'select'.
    document.addEventListener('selectionchange', (ev) => {
      if (document.activeElement === el) {
        dedup();
      }
    });

    // on blur, after a backspace, Chrome moves the start/end selection: fix it
    let scrollLeftOnBlur = false;
    el.addEventListener('blur', (ev) => {
      if (el.scrollLeft) {
        scrollLeftOnBlur = el.scrollLeft || false;
      }
      if (el.selectionStart !== state.start || el.selectionStart !== state.end) {
        [el.selectionStart, el.selectionEnd] = [state.start, state.end];
      }
    }, true);
    el.addEventListener('focusout', (ev) => {
      if (scrollLeftOnBlur !== false) {
        el.scrollLeft = scrollLeftOnBlur;
        scrollLeftOnBlur = false;
      }
    }, true);
  }());

  // add a non-deduped keydown handler, to run before others and intercept space
  el.addEventListener('keydown', (ev) => {
    switch (ev.key) {
    case 'Escape':
      permitNextChange = false;  // force next change
      break;

    case 'ArrowDown':
    case 'Down':
    case 'ArrowUp':
    case 'Up':
      ev.preventDefault();  // disable normal up/down behavior
      return;

    case ' ':
      if (el.dataset.prefix && el.selectionStart === +el.dataset.to) {
        el.dispatchEvent(new CustomEvent('request', {detail: el.dataset.prefix}));
      }

      // TODO: do this to prevent actually space being hit (@samthor prefers it this way)
      //ev.preventDefault();
      break;
    }
  });

  // add a non-deduped keyup handler, for space on mobile browsers
  el.addEventListener('keyup', (ev) => {
    if (ev.keyCode === 229 || !ev.keyCode) {
      // look for a space before whatever was entered.
      const v = el.value.substr(el.selectionStart - 1, 1);
      if (v === ' ' && el.dataset.prefix) {
        el.dispatchEvent(new CustomEvent('request', {detail: el.dataset.prefix}));
      }
    }
  });

  // dedup re-rendering calls
  (function() {
    let frame;
    const dedupRenderLine = () => {
      if (!frame) {
        frame = window.requestAnimationFrame(() => {
          frame = null;
          renderLine();
        });
      }
    };
    window.addEventListener('resize', dedupRenderLine);
    el.addEventListener('wheel', dedupRenderLine, {passive: true});
  }());

  // replace helper
  const replaceFocus = (call) => {
    const previousScrollLeft = el.scrollLeft;
    const [from, to] = [+el.dataset.from, +el.dataset.to];
    const value = el.value.substr(from, to - from);
    let [start, end] = [typer.selectionStart, typer.selectionEnd];

    const update = call(value);
    if (update == null) { return false; }

    // select the region and 'type' it with insertText to provide undo/redo history
    // nb. selecting the typer means that undo will always make us selected; probably fine
    typer.focus();
    typer.selectionStart = from;
    typer.selectionEnd = to;
    const expected = typer.value.substr(0, from) + update + typer.value.substr(to);
    if (!document.execCommand('insertText', false, update) || typer.value !== expected) {
      // set manually: this is fallback / Firefox mode
      typer.value = typer.value.substr(0, from) + update + typer.value.substr(to);
    }

    const drift = (where) => {
      if (where >= to) {
        // after the update
        where = where - (to - from) + update.length;
      } else if (where > from) {
        // during the update
        where = from + update.length;
      } else {
        // do nothing, was before
      }
      return where;
    };

    const prev = document.activeElement;

    typer.focus();
    typer.dispatchEvent(new CustomEvent('change'));  // nb. updates from/to (from won't change)
    // pretend we were like this all along
    [state.start, state.end] = [typer.selectionStart, typer.selectionEnd] = [drift(start), drift(end)];

    prev && prev.focus();

    permitNextChange = true;
    el.scrollLeft = previousScrollLeft;  // before setRange, so the underline is correct
    setRange(from, from + update.length);
    return true;
  };

  // handle 'modifier' event: apply modifiers to the focus word, if any
  el.addEventListener('modifier', (ev) => {
    const arg = {[ev.detail.type]: ev.detail.code};
    replaceFocus((value) => modifier.modify(value, arg).out || '');
  });

  // handle 'emoji' event: if there's a current focus word, then replace it with the new emoji \o/
  el.addEventListener('emoji', (ev) => {
    const emoji = ev.detail.choice;
    if (!replaceFocus(() => emoji)) { return; }

    // listen to the caller's view on what word we should pretend this emoji is
    el.dataset['word'] = ev.detail.word || '';
    datasetSafeDelete(el, 'prefix');
  });
}

upgrade(typer);
<|endoftext|>