<commit_before>import {escapeRegExp, DIRECTION, indexInDirection} from '../core/utils.js';
import Masked from './base.js';

export default
class MaskedNumber extends Masked {
  constructor (opts) {
    super({
      ...MaskedNumber.DEFAULTS,
      ...opts
    });
  }

  _update (opts) {
    if (opts.postFormat) {
      console.warn("'postFormat' option is deprecated and will be removed in next release, use plain options instead.");
      Object.assign(opts, opts.postFormat);
      delete opts.postFormat;
    }
    super._update(opts);
    this._updateRegExps();
  }

  _updateRegExps () {
    // use different regexp to process user input (more strict, input suffix) and tail shifting
    const start = '^'

    let midInput = '';
    let mid = '';
    if (this.allowNegative) {
      midInput += '([+|\\-]?|([+|\\-]?(0|([1-9]+\\d*))))';
      mid += '[+|\\-]?';
    } else {
      midInput += '(0|([1-9]+\\d*))';
    }
    mid += '\\d*';

    let end = (this.scale ?
      '(' + this.radix + '\\d{0,' + this.scale + '})?' :
      '') + '$';

    this._numberRegExpInput = new RegExp(start + midInput + end);
    this._numberRegExp = new RegExp(start + mid + end);
    this._mapToRadixRegExp = new RegExp('[' +
      this.mapToRadix.map(escapeRegExp).join('') +
    ']', 'g');
    this._thousandsSeparatorRegExp = new RegExp(escapeRegExp(this.thousandsSeparator), 'g');
  }

  _extractTail (fromPos=0, toPos=this.value.length) {
    return this._removeThousandsSeparators(super._extractTail(fromPos, toPos));
  }

  _removeThousandsSeparators (value) {
    return value.replace(this._thousandsSeparatorRegExp, '');
  }

  _insertThousandsSeparators (value) {
    // https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
    const parts = value.split(this.radix);
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandsSeparator);
    return parts.join(this.radix);
  }

  doPrepare (str, ...args) {
    return super.doPrepare(this._removeThousandsSeparators(str.replace(this._mapToRadixRegExp, this.radix)), ...args);
  }

  appendWithTail (...args) {
    const oldValueLength = this.value.length;
    this._value = this._removeThousandsSeparators(this.value);
    let startChangePos = this.value.length;

    const appendDetails = super.appendWithTail(...args);
    this._value = this._insertThousandsSeparators(this.value);

    // calculate offsets after insert separators
    let beforeTailPos = startChangePos + appendDetails.inserted.length;
    for (let pos = 0; pos <= beforeTailPos; ++pos) {
      if (this.value[pos] === this.thousandsSeparator) {
        if (pos <= startChangePos) ++startChangePos;
        ++beforeTailPos;
      }
    }

    // adjust details with separators
    appendDetails.rawInserted = appendDetails.inserted;
    appendDetails.inserted = this.value.slice(startChangePos, beforeTailPos);
    appendDetails.shift += startChangePos - oldValueLength;

    return appendDetails;
  }

  nearestInputPos (cursorPos, direction=DIRECTION.LEFT) {
    if (!direction) return cursorPos;

    const nextPos = indexInDirection(cursorPos, direction);
    if (this.value[nextPos] === this.thousandsSeparator) cursorPos += direction;
    return cursorPos;
  }

  doValidate (flags) {
    const regexp = flags.input ? this._numberRegExpInput : this._numberRegExp;

    // validate as string
    let valid = regexp.test(this._removeThousandsSeparators(this.value));

    if (valid) {
      // validate as number
      const number = this.number;
      valid = valid && !isNaN(number) &&
        // check min bound for negative values
        (this.min == null || this.min >= 0 || this.min <= this.number) &&
        // check max bound for positive values
        (this.max == null || this.max <= 0 || this.number <= this.max);
    }

    return valid && super.doValidate(flags);
  }

  doCommit () {
    const number = this.number;
    let validnum = number;

    // check bounds
    if (this.min != null) validnum = Math.max(validnum, this.min);
    if (this.max != null) validnum = Math.min(validnum, this.max);

    if (validnum !== number) this.unmaskedValue = String(validnum);

    let formatted = this.value;

    if (this.normalizeZeros) formatted = this._normalizeZeros(formatted);
    if (this.padFractionalZeros) formatted = this._padFractionalZeros(formatted);

    this._value = formatted;
    super.doCommit();
  }

  _normalizeZeros (value) {
    const parts = this._removeThousandsSeparators(value).split(this.radix);

    // remove leading zeros
    parts[0] = parts[0].replace(/^(\D*)(0*)(\d*)/, (match, sign, zeros, num) => sign + num);
    // add leading zero
    if (value.length && !/\d$/.test(parts[0])) parts[0] = parts[0] + '0';

    if (parts.length > 1) {
      parts[1] = parts[1].replace(/0*$/, '');  // remove trailing zeros
      if (!parts[1].length) parts.length = 1;  // remove fractional
    }

    return this._insertThousandsSeparators(parts.join(this.radix));
  }

  _padFractionalZeros (value) {
    const parts = value.split(this.radix);
    if (parts.length < 2) parts.push('');
    parts[1] = parts[1].padEnd(this.scale, '0');
    return parts.join(this.radix);
  }

  get number () {
    let numstr =
      this._removeThousandsSeparators(
        this._normalizeZeros(
          this.unmaskedValue))
      .replace(this.radix, '.');

    return Number(numstr);
  }

  set number (number) {
    this.unmaskedValue = String(number).replace('.', this.radix);
  }

  get allowNegative () {
    return this.signed ||
      (this.min != null && this.min < 0) ||
      (this.max != null && this.max < 0);
  }
}
MaskedNumber.DEFAULTS = {
  radix: ',',
  thousandsSeparator: '',
  mapToRadix: ['.'],
  scale: 2,
  signed: false,
  normalizeZeros: true,
  padFractionalZeros: false,
};
<commit_msg>fix #14: adjust cursor for number mask<commit_after>import {escapeRegExp, DIRECTION, indexInDirection} from '../core/utils.js';
import Masked from './base.js';

export default
class MaskedNumber extends Masked {
  constructor (opts) {
    super({
      ...MaskedNumber.DEFAULTS,
      ...opts
    });
  }

  _update (opts) {
    if (opts.postFormat) {
      console.warn("'postFormat' option is deprecated and will be removed in next release, use plain options instead.");
      Object.assign(opts, opts.postFormat);
      delete opts.postFormat;
    }
    super._update(opts);
    this._updateRegExps();
  }

  _updateRegExps () {
    // use different regexp to process user input (more strict, input suffix) and tail shifting
    const start = '^'

    let midInput = '';
    let mid = '';
    if (this.allowNegative) {
      midInput += '([+|\\-]?|([+|\\-]?(0|([1-9]+\\d*))))';
      mid += '[+|\\-]?';
    } else {
      midInput += '(0|([1-9]+\\d*))';
    }
    mid += '\\d*';

    let end = (this.scale ?
      '(' + this.radix + '\\d{0,' + this.scale + '})?' :
      '') + '$';

    this._numberRegExpInput = new RegExp(start + midInput + end);
    this._numberRegExp = new RegExp(start + mid + end);
    this._mapToRadixRegExp = new RegExp('[' +
      this.mapToRadix.map(escapeRegExp).join('') +
    ']', 'g');
    this._thousandsSeparatorRegExp = new RegExp(escapeRegExp(this.thousandsSeparator), 'g');
  }

  _extractTail (fromPos=0, toPos=this.value.length) {
    return this._removeThousandsSeparators(super._extractTail(fromPos, toPos));
  }

  _removeThousandsSeparators (value) {
    return value.replace(this._thousandsSeparatorRegExp, '');
  }

  _insertThousandsSeparators (value) {
    // https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
    const parts = value.split(this.radix);
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandsSeparator);
    return parts.join(this.radix);
  }

  doPrepare (str, ...args) {
    return super.doPrepare(this._removeThousandsSeparators(str.replace(this._mapToRadixRegExp, this.radix)), ...args);
  }

  appendWithTail (...args) {
    let wasStartChangePos = this.value.length;
    this._value = this._removeThousandsSeparators(this.value);
    let startChangePos = this.value.length;

    const appendDetails = super.appendWithTail(...args);
    this._value = this._insertThousandsSeparators(this.value);

    // calculate offsets after insert separators
    let beforeTailPos = startChangePos + appendDetails.inserted.length;
    for (let pos = 0; pos <= beforeTailPos; ++pos) {
      if (this.value[pos] === this.thousandsSeparator) {
        if (pos < startChangePos ||
          // check high bound
          (pos === startChangePos && pos < wasStartChangePos)) {
          ++startChangePos;
        }
        if (pos < beforeTailPos) ++beforeTailPos;
      }
    }

    // adjust details with separators
    appendDetails.rawInserted = appendDetails.inserted;
    appendDetails.inserted = this.value.slice(startChangePos, beforeTailPos);
    appendDetails.shift += startChangePos - wasStartChangePos;

    return appendDetails;
  }

  nearestInputPos (cursorPos, direction) {
    if (!direction) return cursorPos;

    const nextPos = indexInDirection(cursorPos, direction);
    if (this.value[nextPos] === this.thousandsSeparator) cursorPos += direction;
    return cursorPos;
  }

  doValidate (flags) {
    const regexp = flags.input ? this._numberRegExpInput : this._numberRegExp;

    // validate as string
    let valid = regexp.test(this._removeThousandsSeparators(this.value));

    if (valid) {
      // validate as number
      const number = this.number;
      valid = valid && !isNaN(number) &&
        // check min bound for negative values
        (this.min == null || this.min >= 0 || this.min <= this.number) &&
        // check max bound for positive values
        (this.max == null || this.max <= 0 || this.number <= this.max);
    }

    return valid && super.doValidate(flags);
  }

  doCommit () {
    const number = this.number;
    let validnum = number;

    // check bounds
    if (this.min != null) validnum = Math.max(validnum, this.min);
    if (this.max != null) validnum = Math.min(validnum, this.max);

    if (validnum !== number) this.unmaskedValue = String(validnum);

    let formatted = this.value;

    if (this.normalizeZeros) formatted = this._normalizeZeros(formatted);
    if (this.padFractionalZeros) formatted = this._padFractionalZeros(formatted);

    this._value = formatted;
    super.doCommit();
  }

  _normalizeZeros (value) {
    const parts = this._removeThousandsSeparators(value).split(this.radix);

    // remove leading zeros
    parts[0] = parts[0].replace(/^(\D*)(0*)(\d*)/, (match, sign, zeros, num) => sign + num);
    // add leading zero
    if (value.length && !/\d$/.test(parts[0])) parts[0] = parts[0] + '0';

    if (parts.length > 1) {
      parts[1] = parts[1].replace(/0*$/, '');  // remove trailing zeros
      if (!parts[1].length) parts.length = 1;  // remove fractional
    }

    return this._insertThousandsSeparators(parts.join(this.radix));
  }

  _padFractionalZeros (value) {
    const parts = value.split(this.radix);
    if (parts.length < 2) parts.push('');
    parts[1] = parts[1].padEnd(this.scale, '0');
    return parts.join(this.radix);
  }

  get number () {
    let numstr =
      this._removeThousandsSeparators(
        this._normalizeZeros(
          this.unmaskedValue))
      .replace(this.radix, '.');

    return Number(numstr);
  }

  set number (number) {
    this.unmaskedValue = String(number).replace('.', this.radix);
  }

  get allowNegative () {
    return this.signed ||
      (this.min != null && this.min < 0) ||
      (this.max != null && this.max < 0);
  }
}
MaskedNumber.DEFAULTS = {
  radix: ',',
  thousandsSeparator: '',
  mapToRadix: ['.'],
  scale: 2,
  signed: false,
  normalizeZeros: true,
  padFractionalZeros: false,
};
<|endoftext|>


<commit_before>import masker from '../masker';
import { filterNumbers } from '../helpers';

const config = {
  us: { thousand: ',', decimal: '.' },
  br: { thousand: '.', decimal: ',' }
};

export default masker(({ locale, value }) => {
  const conf = config[locale || 'us'];

  const patternParts = [`#${conf.thousand}##0`];
  const precision = value || 0;

  if (precision) {
    patternParts.push(
      conf.decimal,
      new Array(value).fill('0').join('')
    );
  }

  return {
    pattern: patternParts.join(''),
    options: { reverse: true },
    pre(value, { delimiter }) {
      if (!value) {
        return '';
      }

      const sign = value.startsWith('-') ? '-' : '';

      const [number, fraction = ''] = value.split(conf.decimal).map(filterNumbers);

      return [sign, delimiter, Number(number), fraction].join('');
    },
    post(value) {
      return value;
    },
  };
});
<commit_msg>[decimal] Bug fix: `0` preceding mask value<commit_after>import masker from '../masker';
import { filterNumbers } from '../helpers';

const config = {
  us: { thousand: ',', decimal: '.' },
  br: { thousand: '.', decimal: ',' }
};

export default masker(({ locale, value }) => {
  const conf = config[locale || 'us'];

  const patternParts = [`#${conf.thousand}##0`];
  const precision = value || 0;

  if (precision) {
    patternParts.push(
      conf.decimal,
      new Array(precision).fill('0').join('')
    );
  }

  return {
    pattern: patternParts.join(''),
    options: { reverse: true },
    pre(value, { delimiter }) {
      if (!value) {
        return '';
      }

      const sign = value.startsWith('-') ? '-' : '';

      let [number, fraction = ''] = value.split(conf.decimal).map(filterNumbers);

      if (fraction && fraction.length > precision) {
        number = `${number}${fraction.slice(0, -precision)}`;
        fraction = fraction.slice(-precision);
      }

      return [sign, delimiter, Number(number), fraction].join('');
    },
    post(value) {
      return value;
    },
  };
});
<|endoftext|>


<commit_before>var promises = require('q'),
	assert = require('assert');

var logger = require('winston').loggers.get('steps'),
	matchers = require('./matchers');

var Feature = new Class( /** @lends Feature# */ {
	/** A sequence of promises to be executed in order, constructed after the scenario for this feature.
	*@private
	*/
	steps: [],

	/** A hash with all widgets accessible to this Feature, indexed on their names.
	*@type	{Object.<String, Widget>}
	*@see	Widget
	*@private
	*/
	widgets: {},

	/**@class	A Feature models a sequence of actions to be executed through Widgets.
	*
	* A feature description file contains a simple descriptive array listing widget methods to execute and widget state descriptors to assert.
	* More formally, such an array is ordered and its members may be:
	* - a closure;
	* - an object whose keys are some widgets' attributes identifiers (ex: "MyWidget.myAttr"), pointing at a string that contains the expected text content of the HTML element represented by the `myAttr` hook in `MyWidget`.
	*
	* Upon instantiation, a Feature translates this array into an array of promises:
	* - closures are executed directly, either as promises if they are so themselves, or as basic functions;
	* - a widget state describing hash maps each of its members to an assertion inside a promise, evaluating all of them asynchronously.
	* All those promises are then evaluated sequentially upon calling the `test` method of a Feature.
	*
	*@constructs
	*@param	{String}	description	A plain text description of the feature, advised to be written in a BDD fashion.
	*@param	{Array}		scenario	An array that describes states and transitions. See class documentation for formatting.
	*@param	{Object.<String, Widget>}	widgets	A hash listing all widgets accessible to this Feature, indexed on their names.
	*/
	initialize: function init(description, scenario, widgets) {
		this.description = description;

		this.widgets = widgets;	//TODO: transform so that they can be referred to with the "Widget" suffix optional?

		this.steps = this.loadScenario(scenario);
	},

	/** Parses an array that describes states and transitions and transforms it into a sequence of promises to be evaluated.
	*
	*@param		{Array}	scenario	An array that describes states and transitions. See class documentation for formatting.
	*@returns	{Array.<function>}	An array of promises representing the given scenario.
	*@private
	*/
	loadScenario: function loadScenario(scenario) {
		var result = [];

		for (var stepIndex = 0; stepIndex < scenario.length; stepIndex++) {
			var step = scenario[stepIndex]; // takes all values listed in a scenario

			/* So, this is going to be a bit hard. Stay with me  ;)
			 * Scenarios are loaded in a different context, absolutely clean by default (see SuiteLoader).
			 * Therefore, steps in the scenario are clean of any prototype augmentation.
			 * MooTools, for example, allows proper type introspection through prototype augmentation. This is not usable here.
			 * But we still need to do introspection to offer proper heuristics. Tricks to achieve this are below.
			 */
			result.push(	typeof step == 'function' ?
							step	// TODO: use .length instead of popping
						  : typeof step == 'object' && step.length >= 0 ?	// an Array has a length property, not an Object; as a consequence, `length` is a reserved property for state description hashes
						    this.buildFunctionalPromise(result.pop(), step)
						  : typeof step == 'object' ?
						    this.buildAssertionPromise(step) // if this is a Hash, it is a description value
						  : this.buildFunctionalPromise(result.pop(), [ step ])	// default: this is a primitive value, we normalize it by wrapping
						);
		}

		return result;
	},

	/** Normalizes an operational closure (i.e. a function that modifies a widget's state) to a format compatible with scenario steps execution.
	*
	*@param	{Function}	func	The raw function to execute.
	*@param	{Array}		params	Parameters to bind to this function.
	*@returns	{Function}	A bound function, ready for execution as a step.
	*@private
	*/
	buildFunctionalPromise: function buildFunctionalPromise(func, params) {
		return func.apply.bind(func, null, params);
	},

	/** Parses a widget state description and creates an assertive closure returning the promise for assertions results upon evaluation.
	*
	*@param		{Object}	hooksVals	A hash whose keys match some widgets' attributes, pointing at values that are expected values for those attributes.
	*@returns	{function}	A parameter-less closure asserting the described state and returning a promise that will be either:
	*	- rejected if any assertion fails, passing a string parameter that describes the first failed match;
	*	- resolved if all assertions pass, with no parameter.
	*@private
	*/
	buildAssertionPromise: function buildAssertionPromise(hooksVals) {
		var matchesLeft = 0,	// optimization: we're using the check loop beneath to cache the count of elements to match
			timeout;	// per-state timeouts. This is independent from implicit waits for elements to appear: this timeout is how long we should wait for a match on a preexistent element

		if (hooksVals.hasOwnProperty('timeout')) {
			timeout = hooksVals.timeout;
			delete hooksVals.timeout;	// we don't want to iterate over this property!
		}

		Object.each(hooksVals, function(expected, attribute) {
			matchesLeft++;

			if (! Object.hasPropertyPath(this.widgets, attribute)) {	// unfortunately, we can't cache this, since WebDriverJS matches elements to the current page once and for all. We'll have to ask access on the page on which the assertion will take place.
				logger.error('Could not find "' + attribute + '" in available widgets. Are you sure you spelled the property path properly?', { widgets: this.widgets });
				throw new Error('Could not find "' + attribute + '" in available widgets');
			}
		}, this);

		return (function() {
			var evaluator = promises.defer();

			var isEmpty = true;	// yep, we have to treat the special case of {}

			/** Callback called after each state descriptor evaluation.
			* Controls the promise fulfilment.
			*@param	{Boolean}	success	Whether the state descriptor assertion got a match or not.
			*@param	{String}	message	Any additional information the assertion found. Will be passed to the promise.
			*/
			var checkStateDescriptionFulfilled = function checkStateDescriptionFulfilled(error) {
				if (error)	// TODO: add a "evaluateAll" option that would reject only after having collected result for all descriptors, not fail on the first failure
					evaluator.reject(error);

				if (--matchesLeft == 0)
					evaluator.resolve();
			}

			Object.each(hooksVals, function(expected, elementSelector) {
				isEmpty = false;

				this.evaluateStateDescriptor(elementSelector, expected, checkStateDescriptionFulfilled, timeout);
			}, this);

			if (isEmpty)
				evaluator.resolve();

			return evaluator.promise;
		}).bind(this);
	},

	/**
	*@param	elementSelector	The widget element whose content is to be evaluated.
	*@param	expected	The expected value for the element content. This is currently a String, but could change when new matchers are added.
	*@param	{Function}	callback	A function to call once the evaluation has ended. Params: {Boolean} was there a match?, {String?} message to justify the result (typically expected/actual comparison).
	*@param	{Number}	[timeout]	Maximum time, in milliseconds, after which the lack of match will be considered as a failure.
	*@private
	*/
	evaluateStateDescriptor: function evaluateStateDescriptor(elementSelector, expected, callback, timeout) {
		var activeMatchers = [];

		if (typeof expected == 'boolean') {	// TODO: make matchers responsible for defining which value types they can handle instead of this horrendous switch
			activeMatchers.push(new matchers.ExistenceMatcher(this.widgets, elementSelector, expected));
		} else if (expected instanceof RegExp) {
			activeMatchers.push(new matchers.RegExpTextMatcher(this.widgets, elementSelector, expected));
		} else {
			activeMatchers.push(new matchers.TextMatcher(this.widgets, elementSelector, expected));
			activeMatchers.push(new matchers.ValueMatcher(this.widgets, elementSelector, expected));
		}

		var matchersLeft = activeMatchers.length;

		function finish(message) {
			activeMatchers.each(function(matcher) {	// first we need to make sure no failed matcher is going to try again to match even after another ended the evaluation
				matcher.cancel();
			});

			callback(message);	// then we actually call the callback
		}

		function handleSuccess() {
			finish();
		}

		function handleFailure(message) {
			if (--matchersLeft <= 0)
				finish(message)
		}

		activeMatchers.each(function(matcher) {
			matcher.test(timeout)
				   .then(handleSuccess, handleFailure)
				   .end();	// rethrow any exception
		});

		if (activeMatchers.length <= 0)
			throw new Error('No matcher found for the given value type  :-/  (had to check for "' + expected + '").');
	},

	/** Asynchronously evaluates the scenario given to this feature.
	*
	*@returns	{Promise}	A promise that will be either:
	*	- rejected if any assertion or action fails, passing a hash containing two keys:
	*		• `failures`: an array of strings that describe reason(s) for failure(s) (one reason per item in the array);
	*		• `errors`: an array of strings that describe errors that arose when trying to evaluate the feature.
	*	- resolved if all assertions pass, with no parameter.
	*/
	test: function evaluate() {
		var deferred = promises.defer(),
			stepIndex = -1;

		var evaluateNext,
			failureReasons = {
				failures: [],	// we differentiate between the two types
				errors: []
			};

		var handleFailure = function handleFailure(message, step) {
			failureReasons.failures.push(message + ' (at step ' + (step + 1) + ')');	// make user-visible indices 1-based
			evaluateNext();
		}

		function fulfillPromise(report) {
			if (report.failures.length || report.errors.length)
				return deferred.reject(report);
			else
				return deferred.resolve();
		}

		evaluateNext = (function evalNext() {
			stepIndex++;

			if (stepIndex == this.steps.length)
				return fulfillPromise(failureReasons);

			try {
				var result = this.steps[stepIndex]();
				// unfortunately, [q.when](https://github.com/kriskowal/q#the-middle) is not compatible with WebDriver's Promises/A implementation, and we need to explicitly call `then` to reject thrown exceptions
				if (result && typeof result.then == 'function') {
					result.then(evaluateNext, function(message) {
						handleFailure(message, stepIndex)
					});
				} else {
					evaluateNext();
				}
			} catch (error) {
				failureReasons.errors.push(error);
				evaluateNext();
			}
		}).bind(this);

		evaluateNext();	//TODO: make async

		return deferred.promise;
	}
});

module.exports = Feature;	// CommonJS export
<commit_msg>Corrected RegExp matching in state descriptors.<commit_after>var promises = require('q'),
	assert = require('assert');

var logger = require('winston').loggers.get('steps'),
	matchers = require('./matchers');

var Feature = new Class( /** @lends Feature# */ {
	/** A sequence of promises to be executed in order, constructed after the scenario for this feature.
	*@private
	*/
	steps: [],

	/** A hash with all widgets accessible to this Feature, indexed on their names.
	*@type	{Object.<String, Widget>}
	*@see	Widget
	*@private
	*/
	widgets: {},

	/**@class	A Feature models a sequence of actions to be executed through Widgets.
	*
	* A feature description file contains a simple descriptive array listing widget methods to execute and widget state descriptors to assert.
	* More formally, such an array is ordered and its members may be:
	* - a closure;
	* - an object whose keys are some widgets' attributes identifiers (ex: "MyWidget.myAttr"), pointing at a string that contains the expected text content of the HTML element represented by the `myAttr` hook in `MyWidget`.
	*
	* Upon instantiation, a Feature translates this array into an array of promises:
	* - closures are executed directly, either as promises if they are so themselves, or as basic functions;
	* - a widget state describing hash maps each of its members to an assertion inside a promise, evaluating all of them asynchronously.
	* All those promises are then evaluated sequentially upon calling the `test` method of a Feature.
	*
	*@constructs
	*@param	{String}	description	A plain text description of the feature, advised to be written in a BDD fashion.
	*@param	{Array}		scenario	An array that describes states and transitions. See class documentation for formatting.
	*@param	{Object.<String, Widget>}	widgets	A hash listing all widgets accessible to this Feature, indexed on their names.
	*/
	initialize: function init(description, scenario, widgets) {
		this.description = description;

		this.widgets = widgets;	//TODO: transform so that they can be referred to with the "Widget" suffix optional?

		this.steps = this.loadScenario(scenario);
	},

	/** Parses an array that describes states and transitions and transforms it into a sequence of promises to be evaluated.
	*
	*@param		{Array}	scenario	An array that describes states and transitions. See class documentation for formatting.
	*@returns	{Array.<function>}	An array of promises representing the given scenario.
	*@private
	*/
	loadScenario: function loadScenario(scenario) {
		var result = [];

		for (var stepIndex = 0; stepIndex < scenario.length; stepIndex++) {
			var step = scenario[stepIndex]; // takes all values listed in a scenario

			/* So, this is going to be a bit hard. Stay with me  ;)
			 * Scenarios are loaded in a different context, absolutely clean by default (see SuiteLoader).
			 * Therefore, steps in the scenario are clean of any prototype augmentation.
			 * MooTools, for example, allows proper type introspection through prototype augmentation. This is not usable here.
			 * But we still need to do introspection to offer proper heuristics. Tricks to achieve this are below.
			 */
			result.push(	typeof step == 'function' ?
							step	// TODO: use .length instead of popping
						  : typeof step == 'object' && step.length >= 0 ?	// an Array has a length property, not an Object; as a consequence, `length` is a reserved property for state description hashes
						    this.buildFunctionalPromise(result.pop(), step)
						  : typeof step == 'object' ?
						    this.buildAssertionPromise(step) // if this is a Hash, it is a description value
						  : this.buildFunctionalPromise(result.pop(), [ step ])	// default: this is a primitive value, we normalize it by wrapping
						);
		}

		return result;
	},

	/** Normalizes an operational closure (i.e. a function that modifies a widget's state) to a format compatible with scenario steps execution.
	*
	*@param	{Function}	func	The raw function to execute.
	*@param	{Array}		params	Parameters to bind to this function.
	*@returns	{Function}	A bound function, ready for execution as a step.
	*@private
	*/
	buildFunctionalPromise: function buildFunctionalPromise(func, params) {
		return func.apply.bind(func, null, params);
	},

	/** Parses a widget state description and creates an assertive closure returning the promise for assertions results upon evaluation.
	*
	*@param		{Object}	hooksVals	A hash whose keys match some widgets' attributes, pointing at values that are expected values for those attributes.
	*@returns	{function}	A parameter-less closure asserting the described state and returning a promise that will be either:
	*	- rejected if any assertion fails, passing a string parameter that describes the first failed match;
	*	- resolved if all assertions pass, with no parameter.
	*@private
	*/
	buildAssertionPromise: function buildAssertionPromise(hooksVals) {
		var matchesLeft = 0,	// optimization: we're using the check loop beneath to cache the count of elements to match
			timeout;	// per-state timeouts. This is independent from implicit waits for elements to appear: this timeout is how long we should wait for a match on a preexistent element

		if (hooksVals.hasOwnProperty('timeout')) {
			timeout = hooksVals.timeout;
			delete hooksVals.timeout;	// we don't want to iterate over this property!
		}

		Object.each(hooksVals, function(expected, attribute) {
			matchesLeft++;

			if (! Object.hasPropertyPath(this.widgets, attribute)) {	// unfortunately, we can't cache this, since WebDriverJS matches elements to the current page once and for all. We'll have to ask access on the page on which the assertion will take place.
				logger.error('Could not find "' + attribute + '" in available widgets. Are you sure you spelled the property path properly?', { widgets: this.widgets });
				throw new Error('Could not find "' + attribute + '" in available widgets');
			}
		}, this);

		return (function() {
			var evaluator = promises.defer();

			var isEmpty = true;	// yep, we have to treat the special case of {}

			/** Callback called after each state descriptor evaluation.
			* Controls the promise fulfilment.
			*@param	{Boolean}	success	Whether the state descriptor assertion got a match or not.
			*@param	{String}	message	Any additional information the assertion found. Will be passed to the promise.
			*/
			var checkStateDescriptionFulfilled = function checkStateDescriptionFulfilled(error) {
				if (error)	// TODO: add a "evaluateAll" option that would reject only after having collected result for all descriptors, not fail on the first failure
					evaluator.reject(error);

				if (--matchesLeft == 0)
					evaluator.resolve();
			}

			Object.each(hooksVals, function(expected, elementSelector) {
				isEmpty = false;

				this.evaluateStateDescriptor(elementSelector, expected, checkStateDescriptionFulfilled, timeout);
			}, this);

			if (isEmpty)
				evaluator.resolve();

			return evaluator.promise;
		}).bind(this);
	},

	/**
	*@param	elementSelector	The widget element whose content is to be evaluated.
	*@param	expected	The expected value for the element content. This is currently a String, but could change when new matchers are added.
	*@param	{Function}	callback	A function to call once the evaluation has ended. Params: {Boolean} was there a match?, {String?} message to justify the result (typically expected/actual comparison).
	*@param	{Number}	[timeout]	Maximum time, in milliseconds, after which the lack of match will be considered as a failure.
	*@private
	*/
	evaluateStateDescriptor: function evaluateStateDescriptor(elementSelector, expected, callback, timeout) {
		var activeMatchers = [];

		if (typeof expected == 'boolean') {	// TODO: make matchers responsible for defining which value types they can handle instead of this horrendous switch
			activeMatchers.push(new matchers.ExistenceMatcher(this.widgets, elementSelector, expected));
		} else if (expected.constructor && expected.constructor.name === 'RegExp') {	// since elements are loaded in a separate context, the `instanceof` fails, as it compares constructors references
			activeMatchers.push(new matchers.RegExpTextMatcher(this.widgets, elementSelector, expected));
		} else {
			activeMatchers.push(new matchers.TextMatcher(this.widgets, elementSelector, expected));
			activeMatchers.push(new matchers.ValueMatcher(this.widgets, elementSelector, expected));
		}

		var matchersLeft = activeMatchers.length;

		function finish(message) {
			activeMatchers.each(function(matcher) {	// first we need to make sure no failed matcher is going to try again to match even after another ended the evaluation
				matcher.cancel();
			});

			callback(message);	// then we actually call the callback
		}

		function handleSuccess() {
			finish();
		}

		function handleFailure(message) {
			if (--matchersLeft <= 0)
				finish(message)
		}

		activeMatchers.each(function(matcher) {
			matcher.test(timeout)
				   .then(handleSuccess, handleFailure)
				   .end();	// rethrow any exception
		});

		if (activeMatchers.length <= 0)
			throw new Error('No matcher found for the given value type  :-/  (had to check for "' + expected + '").');
	},

	/** Asynchronously evaluates the scenario given to this feature.
	*
	*@returns	{Promise}	A promise that will be either:
	*	- rejected if any assertion or action fails, passing a hash containing two keys:
	*		• `failures`: an array of strings that describe reason(s) for failure(s) (one reason per item in the array);
	*		• `errors`: an array of strings that describe errors that arose when trying to evaluate the feature.
	*	- resolved if all assertions pass, with no parameter.
	*/
	test: function evaluate() {
		var deferred = promises.defer(),
			stepIndex = -1;

		var evaluateNext,
			failureReasons = {
				failures: [],	// we differentiate between the two types
				errors: []
			};

		var handleFailure = function handleFailure(message, step) {
			failureReasons.failures.push(message + ' (at step ' + (step + 1) + ')');	// make user-visible indices 1-based
			evaluateNext();
		}

		function fulfillPromise(report) {
			if (report.failures.length || report.errors.length)
				return deferred.reject(report);
			else
				return deferred.resolve();
		}

		evaluateNext = (function evalNext() {
			stepIndex++;

			if (stepIndex == this.steps.length)
				return fulfillPromise(failureReasons);

			try {
				var result = this.steps[stepIndex]();
				// unfortunately, [q.when](https://github.com/kriskowal/q#the-middle) is not compatible with WebDriver's Promises/A implementation, and we need to explicitly call `then` to reject thrown exceptions
				if (result && typeof result.then == 'function') {
					result.then(evaluateNext, function(message) {
						handleFailure(message, stepIndex)
					});
				} else {
					evaluateNext();
				}
			} catch (error) {
				failureReasons.errors.push(error);
				evaluateNext();
			}
		}).bind(this);

		evaluateNext();	//TODO: make async

		return deferred.promise;
	}
});

module.exports = Feature;	// CommonJS export
<|endoftext|>


<commit_before>// Define history model and handlers

NetVis.prototype._constructNetVisHistory = function() {
	var self = this;
	self.history = new BaseNetVisModel(this); // History class inherits from baseModel

	self.history.loadEvent = function(obj, momentTime) {
		obj._t = momentTime;
		obj.time = momentTime.toISOString();
		// eventID to be unique and contain timestamp
		// of form "<timestamp>#3" where 3 is count of events with
		// the same timestamp
		var i=1;
		obj.id = obj.time;
		while(this._asObject[obj.id]){
			i++;
			obj.id = obj.time + "#" + i;
		}
		obj.id = obj.time;
		this._asObject[obj.id] = obj;

		// insert event so that asArray is sorted
		// with binary search for appropriate position
		var cur = 0,
			lowI = 0,
			highI = this.asArray.length;
		while (lowI < highI) {
			cur = Math.floor((highI + lowI) /2);
			if (this.asArray[cur]._t.isBefore(obj._t)) {
				lowI = cur + 1;
			} else {
				highI = cur;
			}
		}
		this.asArray.splice(Math.floor((highI + lowI) /2), 0,obj);
	};

	self.history.updateAll = function() {
		// create interval instances from events array
		if (!this.asArray) {
			// no events or not initialized
			return;
		}
		this.intervals = [];
		var curInterval = 0;

		// sorting all the events by happenning at the same timestamps
		// and making time interval instances
		cur = 0;
		startEvents = [this.asArray[cur]];
		cur ++;
		while (cur < this.asArray.length -1 && !this.asArray[cur]._t.isAfter(startEvents[0]._t)) {
			startEvents.push(this.asArray[cur]);
			cur++;
		}

		// traversing all simultanious events into events for interval boundaries
		while (cur < this.asArray.length) {
			finishEvents =[this.asArray[cur]];
			cur++;
			while (cur < this.asArray.length -1 && !this.asArray[cur]._t.isAfter(finishEvents[0]._t)) {
				finishEvents.push(this.asArray[cur]);
				cur++;
			}

			curInterval = new NetVisInterval(startEvents, finishEvents, curInterval);
			if (this.intervals.length === 0) {
				for(var i=0; i< self.Nodes.asArray.length; i++) {
					if (self.Nodes.asArray[i].permanentNode) {
						curInterval.nodes.push(self.Nodes.asArray[i]);
					}
				}
			}
			this.intervals.push(curInterval);
			startEvents = finishEvents;
		}

		// build doubly linked list for time intervals
		for (var j=0; j< this.intervals.length -1; j++) {
			this.intervals[j].next = this.intervals[j+1];
			this.intervals[j+1].prev = this.intervals[j];
		}

	};

	self.history.next = function() {
		if (self.selectedTimeInterval && self.selectedTimeInterval.next) {
			self.selectedTimeInterval = self.selectedTimeInterval.next;
		}
	};

	// add default time margin moments
	self.history.loadEvent({"tag":"end"},moment("3000-01-01"));
	self.history.loadEvent({"tag":"start"},moment("1970-01-01"));
	self.history.updateAll();

};
<commit_msg>Store timeinterval position index as attribute<commit_after>// Define history model and handlers

NetVis.prototype._constructNetVisHistory = function() {
	var self = this;
	self.history = new BaseNetVisModel(this); // History class inherits from baseModel

	self.history.loadEvent = function(obj, momentTime) {
		obj._t = momentTime;
		obj.time = momentTime.toISOString();
		// eventID to be unique and contain timestamp
		// of form "<timestamp>#3" where 3 is count of events with
		// the same timestamp
		var i=1;
		obj.id = obj.time;
		while(this._asObject[obj.id]){
			i++;
			obj.id = obj.time + "#" + i;
		}
		obj.id = obj.time;
		this._asObject[obj.id] = obj;

		// insert event so that asArray is sorted
		// with binary search for appropriate position
		var cur = 0,
			lowI = 0,
			highI = this.asArray.length;
		while (lowI < highI) {
			cur = Math.floor((highI + lowI) /2);
			if (this.asArray[cur]._t.isBefore(obj._t)) {
				lowI = cur + 1;
			} else {
				highI = cur;
			}
		}
		this.asArray.splice(Math.floor((highI + lowI) /2), 0,obj);
	};

	self.history.updateAll = function() {
		// create interval instances from events array
		if (!this.asArray) {
			// no events or not initialized
			return;
		}
		this.intervals = [];
		var curInterval = 0;

		// sorting all the events by happenning at the same timestamps
		// and making time interval instances
		cur = 0;
		startEvents = [this.asArray[cur]];
		cur ++;
		while (cur < this.asArray.length -1 && !this.asArray[cur]._t.isAfter(startEvents[0]._t)) {
			startEvents.push(this.asArray[cur]);
			cur++;
		}

		// traversing all simultanious events into events for interval boundaries
		while (cur < this.asArray.length) {
			finishEvents =[this.asArray[cur]];
			cur++;
			while (cur < this.asArray.length -1 && !this.asArray[cur]._t.isAfter(finishEvents[0]._t)) {
				finishEvents.push(this.asArray[cur]);
				cur++;
			}

			curInterval = new NetVisInterval(startEvents, finishEvents, curInterval);
			if (this.intervals.length === 0) {
				for(var i=0; i< self.Nodes.asArray.length; i++) {
					if (self.Nodes.asArray[i].permanentNode) {
						curInterval.nodes.push(self.Nodes.asArray[i]);
					}
				}
			}
			curInterval.i = this.intervals.length; // store position index to assign timeline slider to the corresponding position
			this.intervals.push(curInterval);
			startEvents = finishEvents;
		}

		// build doubly linked list for time intervals
		for (var j=0; j< this.intervals.length -1; j++) {

			this.intervals[j].next = this.intervals[j+1];
			this.intervals[j+1].prev = this.intervals[j];
		}

	};

	self.history.next = function() {
		if (self.selectedTimeInterval && self.selectedTimeInterval.next) {
			self.selectedTimeInterval = self.selectedTimeInterval.next;
		}
	};

	// add default time margin moments
	self.history.loadEvent({"tag":"end"},moment("3000-01-01"));
	self.history.loadEvent({"tag":"start"},moment("1970-01-01"));
	self.history.updateAll();

};
<|endoftext|>


<commit_before>'use strict'

const Config = require('../config.json')
const Fetcher = require('./Fetcher')
const GitHubAuth = require('./GitHubAuth')

const repoUrlPrefix = 'https://api.github.com/repos/'

function getTask(data) {
  const repoUrl = data.repository_url
  const repository = repoUrl.slice(repoUrlPrefix.length)
  const repositoryOwner = repository.split('/')[0]
  const type = typeof data.pull_request === 'object' ? 'pull' : 'issue'
  return {
  let apiUrl = data.url
  if (type === 'pull') {
    apiUrl = apiUrl.replace(/\/issues\//, '/pulls/')
  }
    storageKey: `${type}-${data.id}`,
    id: data.id,
    type,
    title: data.title,
    body: data.body,
    state: data.state,
    createdAt: data.created_at,
    updatedAt: data.updated_at,
    closedAt: data.closed_at,
    isPullRequest: type === 'pull',
    repositoryApiUrl: repoUrl,
    url: data.html_url,
    apiUrl,
    number: data.number,
    repository,
    repositoryOwner,
    repositoryOwnerUrl: `https://github.com/${repositoryOwner}`,
    repositoryOwnerAvatar: `https://github.com/${repositoryOwner}.png?size=30`,
    user: data.user.login,
    userUrl: data.user.html_url,
    userAvatar: `https://github.com/${data.user.login}.png?size=16`,
    userType: data.user.type,
  }
}

class GitHub extends Fetcher {
  constructor(token) {
    super()
    this.token = token
  }

  // https://developer.github.com/v3/activity/notifications/#list-your-notifications
  getNotifications() {
    return this.get('notifications')
  }

  // https://developer.github.com/v3/search/#search-issues
  getTasks(query = Config.searchQuery) {
    const urlPath = `search/issues?q=${encodeURIComponent(query)}`
    return this.get(urlPath).then(({ items }) => items.map(d => getTask(d)))
  }

  // https://developer.github.com/v3/users/#get-the-authenticated-user
  getCurrentUser() {
    return this.get('user')
  }

  get(relativeOrAbsoluteUrl, previousJson) {
    let url = relativeOrAbsoluteUrl
    if (url.indexOf('http') !== 0) {
      url = `${Config.githubApiUrl}/${relativeOrAbsoluteUrl}`
    }
    if (!this.token) {
      this.token = GitHubAuth.getToken()
    }
    const opts = {
      headers: {
        Accept: 'application/vnd.github.v3+json',
        Authorization: `token ${this.token}`,
      },
    }
    return new Promise((resolve, reject) => super.get(url, opts).then(res => {
      const { json, headers } = res
      const combinedJson = this.combineJson(json, previousJson)
      const link = headers.get('Link')
      if (!link) {
        return resolve(combinedJson)
      }
      const nextUrl = this.getNextUrl(link)
      if (nextUrl) {
        return this.get(nextUrl, combinedJson).then(resolve).catch(reject)
      }
      return resolve(combinedJson)
    }).catch(reject))
  }

  combineJson(json1, json2) {
    if (typeof json2 === 'undefined') {
      return json1
    }
    const is1Array = json1.constructor.name === 'Array'
    const is2Array = json2.constructor.name === 'Array'
    if (is1Array && is2Array) {
      return json1.concat(json2)
    }
    return Object.assign({}, json1, json2)
  }

  // Sample input:
  // Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
  // <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
  // <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
  // <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"
  //
  // Sample output:
  // https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15
  getNextUrl(link) {
    const urlsAndRels = link.split(',')
    let nextUrl
    urlsAndRels.forEach(str => {
      const urlAndRel = str.trim().split('; ')
      if (urlAndRel[1] === 'rel="next"') {
        const urlInBrackets = urlAndRel[0]
        nextUrl = urlInBrackets.slice(1, urlInBrackets.length - 1)
        return
      }
    })
    return nextUrl
  }
}

module.exports = GitHub
<commit_msg>Grab notifications from the last month<commit_after>'use strict'

const Config = require('../config.json')
const Fetcher = require('./Fetcher')
const GitHubAuth = require('./GitHubAuth')

const repoUrlPrefix = 'https://api.github.com/repos/'

function getTask(data) {
  const repoUrl = data.repository_url
  const repository = repoUrl.slice(repoUrlPrefix.length)
  const repositoryOwner = repository.split('/')[0]
  const type = typeof data.pull_request === 'object' ? 'pull' : 'issue'
  let apiUrl = data.url
  if (type === 'pull') {
    apiUrl = apiUrl.replace(/\/issues\//, '/pulls/')
  }
  return {
    storageKey: `${type}-${data.id}`,
    id: data.id,
    type,
    title: data.title,
    body: data.body,
    state: data.state,
    createdAt: data.created_at,
    updatedAt: data.updated_at,
    closedAt: data.closed_at,
    isPullRequest: type === 'pull',
    repositoryApiUrl: repoUrl,
    url: data.html_url,
    apiUrl,
    number: data.number,
    repository,
    repositoryOwner,
    repositoryOwnerUrl: `https://github.com/${repositoryOwner}`,
    repositoryOwnerAvatar: `https://github.com/${repositoryOwner}.png?size=30`,
    user: data.user.login,
    userUrl: data.user.html_url,
    userAvatar: `https://github.com/${data.user.login}.png?size=16`,
    userType: data.user.type,
  }
}

class GitHub extends Fetcher {
  constructor(token) {
    super()
    this.token = token
  }

  // https://developer.github.com/v3/activity/notifications/#list-your-notifications
  getNotifications() {
    const pastDate = new Date()
    pastDate.setDate(pastDate.getDate() - 31)
    const dateStr = pastDate.toISOString()
    return this.get(`notifications?since=${encodeURIComponent(dateStr)}`)
  }

  // https://developer.github.com/v3/search/#search-issues
  getTasks(query = Config.searchQuery) {
    const urlPath = `search/issues?q=${encodeURIComponent(query)}`
    return this.get(urlPath).then(({ items }) => items.map(d => getTask(d)))
  }

  // https://developer.github.com/v3/users/#get-the-authenticated-user
  getCurrentUser() {
    return this.get('user')
  }

  get(relativeOrAbsoluteUrl, previousJson) {
    let url = relativeOrAbsoluteUrl
    if (url.indexOf('http') !== 0) {
      url = `${Config.githubApiUrl}/${relativeOrAbsoluteUrl}`
    }
    if (!this.token) {
      this.token = GitHubAuth.getToken()
    }
    const opts = {
      headers: {
        Accept: 'application/vnd.github.v3+json',
        Authorization: `token ${this.token}`,
      },
    }
    return new Promise((resolve, reject) => super.get(url, opts).then(res => {
      const { json, headers } = res
      const combinedJson = this.combineJson(json, previousJson)
      const link = headers.get('Link')
      if (!link) {
        return resolve(combinedJson)
      }
      const nextUrl = this.getNextUrl(link)
      if (nextUrl) {
        return this.get(nextUrl, combinedJson).then(resolve).catch(reject)
      }
      return resolve(combinedJson)
    }).catch(reject))
  }

  combineJson(json1, json2) {
    if (typeof json2 === 'undefined') {
      return json1
    }
    const is1Array = json1.constructor.name === 'Array'
    const is2Array = json2.constructor.name === 'Array'
    if (is1Array && is2Array) {
      return json1.concat(json2)
    }
    return Object.assign({}, json1, json2)
  }

  // Sample input:
  // Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
  // <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
  // <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
  // <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"
  //
  // Sample output:
  // https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15
  getNextUrl(link) {
    const urlsAndRels = link.split(',')
    let nextUrl
    urlsAndRels.forEach(str => {
      const urlAndRel = str.trim().split('; ')
      if (urlAndRel[1] === 'rel="next"') {
        const urlInBrackets = urlAndRel[0]
        nextUrl = urlInBrackets.slice(1, urlInBrackets.length - 1)
        return
      }
    })
    return nextUrl
  }
}

module.exports = GitHub
<|endoftext|>


<commit_before>function is (object) {
  return object &&
    typeof object === 'object' &&
    Object.getPrototypeOf(object) === Object.getPrototypeOf({})
}

export { is }
<commit_msg>add object module and is function<commit_after>function is(object) {
  return object &&
    typeof object === 'object' &&
    Object.getPrototypeOf(object) === Object.getPrototypeOf({})
}

export { is }<|endoftext|>


<commit_before>import { getNodeIndex } from '../view/selection';
import { deltaFromDom } from '../view/dom';
import diff from '../diff';

const SOURCE_USER = 'user';
const lastWord = /\w+[^\w]*$/;
const firstWord = /^[^\w]*\w+/;
const lastLine = /[^\n]*$/;
const firstLine = /^[^\n]*/;
let pasteMutation = false;

// Basic text input module. Prevent any actions other than typing characters and handle with the API.
export default function input() {
  return view => {
    const editor = view.editor;
    const mutationOptions = {
      characterData: true,
      characterDataOldValue: true,
      subtree: true,
      childList: true,
      attributes: true
    };

    // Detects changes from spell-check and the user typing
    function onMutate(list) {
      list = list.filter(m => m.target !== view.root);
      if (!list.length) return;

      const selection = view.getSelection();
      const textChange = getTextChange(list);

      // Only one text node has been altered. Optimize for view most common case, which is typing in a paragraph.
      if (textChange) {
        if (textChange.ops.length) {
          // console.log('changing a little', textChange);
          if (!editor.updateContents(textChange, SOURCE_USER, selection)) {
            view.render();
          }
        }
      } else {
        let contents = deltaFromDom(view, view.root, { ignoreAttributes: true });
        if (pasteMutation) {
          const from = editor.selection ? editor.selection[0] : undefined;
          const to = selection ? selection[1] : undefined;
          contents = contents.clean(from, to);
        }
        contents = contents.compose(view.reverseDecorators);
        // Decorators may extend beyond the text, pull those out
        contents.ops = contents.ops.filter(op => op.insert);
        const change = editor.contents.diff(contents);
        // console.log('changing a lot (possibly)', change);
        if (!editor.updateContents(change, SOURCE_USER, selection)) {
          view.render();
        }
      }
    }

    function onEnter(event, shortcut) {
      if (event.defaultPrevented) return;
      event.preventDefault();
      let [ from, to ] = editor.getSelectedRange();

      const line = editor.contents.getLine(from);
      let attributes = line.attributes;
      const block = view.paper.blocks.find(attributes);
      const isDefault = !block || block === view.paper.blocks.getDefault();
      const length = line.end - line.start - 1;
      const atEnd = to === line.end - 1;
      if (atEnd && !isDefault && block.defaultFollows) {
        attributes = {};
      }
      if (!length && !isDefault && !block.defaultFollows && from === to) {
        editor.formatLine(from, to, {}, SOURCE_USER);
      } else {
        let selection = from + 1;
        if (from === to && atEnd) {
          from++;
          to++;
        }
        editor.insertText(from, to, '\n', attributes, SOURCE_USER, selection);
      }
    }

    function onShiftEnter(event) {
      if (event.defaultPrevented) return;
      event.preventDefault();
      let [ from, to ] = editor.getSelectedRange();
      editor.insertEmbed(from, to, 'br', true, null, SOURCE_USER);
    }

    function onBackspace(event, shortcut) {
      if (event.defaultPrevented || shortcut === 'Alt+Backspace' || shortcut === 'Mod+Backspace') return;
      event.preventDefault();

      let [ from, to ] = editor.getSelectedRange();
      if (from + to === 0) {
        const line = editor.contents.getLine(from);
        const block = view.paper.blocks.find(line.attributes);
        if (block) editor.formatLine(0, {}, SOURCE_USER);
      } else {
        // The "from" block needs to stay the same. The "to" block gets merged into it
        if (from === to) {
          if ((shortcut === 'Alt+Backspace' && view.isMac) || (shortcut === 'Mod+Backspace' && !view.isMac)) {
            const match = editor.getText().slice(0, from).match(lastWord);
            if (match) from -= match[0].length;
          } else if (shortcut === 'Mod+Backspace' && view.isMac) {
            const match = editor.getText().slice(0, from).match(lastLine);
            if (match) from -= match[0].length;
          } else {
            const line = editor.contents.getLine(from);
            if (from === line.start) {
              const blocks = view.paper.blocks;
              const block = blocks.find(line.attributes);
              if (block && block !== blocks.getDefault() && !block.defaultFollows) {
                editor.formatLine(from, {}, SOURCE_USER);
                return;
              }
            }

            from--;
          }
        }
        editor.deleteText(from, to, SOURCE_USER);
      }
    }

    function onDelete(event, shortcut) {
      if (event.defaultPrevented || shortcut === 'Alt+Delete') return;
      event.preventDefault();

      let [ from, to ] = editor.getSelectedRange();
      if (from === to && from === editor.length) return;

      if (from === to) {
        if (shortcut === 'Alt+Delete' && view.isMac) {
          const match = editor.getText().slice(from).match(firstWord);
          if (match) to += match[0].length;
        } else {
          to++;
        }
      }
      editor.deleteText(from, to, SOURCE_USER);
    }

    function onTab(event, shortcut) {
      if (event.defaultPrevented) return;
      event.preventDefault();

      const direction = shortcut === 'Tab' || shortcut === 'Mod+]' ? 1 : -1;
      const [ from, to ] = editor.getSelectedRange();
      const lines = editor.contents.getLines(from, to);

      editor.transaction(() => {
        lines.forEach((line, i) => {
          if (line.attributes.list) {
            let prevLine = lines[i -1];
            if (!prevLine && line.index > 0) prevLine = editor.contents.getLine(line.start - 1);
            const prevIndent = prevLine && prevLine.attributes.list ? prevLine.attributes.indent || 0 : -1;

            let indent = line.attributes.indent || 0;
            indent += direction;
            if (indent > prevIndent + 1) return console.log('will not indent too much');
            if (indent < 0) {
              editor.formatLine(line.start, {});
            } else {
              const attributes = { ...line.attributes, indent };
              editor.formatLine(line.start, attributes);
            }
          }
        });
      }, SOURCE_USER);
    }

    function getTextChange(mutations) {
      const mutation = getTextChangeMutation(mutations);
      if (!mutation) return;

      const change = editor.delta();
      const node = mutation.type === 'characterData' ? mutation.target : mutation.addedNodes[0];
      const index = view.reverseDecorators.transform(getNodeIndex(view, node));
      change.retain(index);

      if (mutation.type === 'characterData') {
        const diffs = diff(mutation.oldValue.replace(/\xA0/g, ' '), mutation.target.nodeValue.replace(/\xA0/g, ' '));
        diffs.forEach(([ action, string ]) => {
          if (action === diff.EQUAL) change.retain(string.length);
          else if (action === diff.DELETE) change.delete(string.length);
          else if (action === diff.INSERT) {
            change.insert(string, editor.activeFormats);
          }
        });
        change.chop();
      } else {
        change.insert(node.nodeValue.replace(/\xA0/g, ' '), editor.activeFormats);
      }

      return change;
    }

    function getTextChangeMutation(mutations) {
      const seen = new Set();
      mutations = mutations.filter(m => {
        if (m.type !== 'characterData') return true;
        if (seen.has(m.target)) return false;
        seen.add(m.target);
        return true;
      });

      if (mutations.length > 3) return;

      const first = mutations[0];
      const second = mutations[1];
      const third = mutations[2];
      const added = first.addedNodes[0];
      const removed = second && second.removedNodes[0];

      if (mutations.length === 1) {
        if (first.type === 'characterData') {
          return first;
        } else if (first.type === 'childList' && added && added.nodeType === Node.TEXT_NODE) {
          return first;
        }
      } else {
        if (first.type === 'childList' && added && added.nodeType === Node.TEXT_NODE) {
          const textNode = third || second;
          if (removed && removed.nodeName !== 'BR') return;
          if (textNode.type === 'characterData' && textNode.target === added) {
            return first;
          }
        }
      }
    }

    function onPaste() {
      pasteMutation = true;
      setTimeout(() => {
        pasteMutation = false;
      }, 50);
    }

    const observer = new MutationObserver(onMutate);
    observer.observe(view.root, mutationOptions);

    // Don't observe the changes that occur when the view updates, we only want to respond to changes that happen
    // outside of our API to read them back in
    function onRendering() {
      observer.disconnect();
    }

    // Once the view update is complete, continue observing for changes
    function onRender() {
      observer.observe(view.root, mutationOptions);
    }

    view.on('rendering', onRendering);
    view.on('render', onRender);
    view.on('shortcut:Enter', onEnter);
    view.on('shortcut:Shift+Enter', onShiftEnter);
    view.on('shortcut:Backspace', onBackspace);
    view.on('shortcut:Alt+Backspace', onBackspace);
    view.on('shortcut:Mod+Backspace', onBackspace);
    view.on('shortcut:Delete', onDelete);
    view.on('shortcut:Alt+Delete', onDelete);
    view.on('shortcut:Tab', onTab);
    view.on('shortcut:Shift+Tab', onTab);
    view.root.addEventListener('paste', onPaste);

    return {
      destroy() {
        observer.disconnect();
        view.off('rendering', onRendering);
        view.off('render', onRender);
        view.off('shortcut:Enter', onEnter);
        view.off('shortcut:Shift+Enter', onShiftEnter);
        view.off('shortcut:Backspace', onBackspace);
        view.off('shortcut:Alt+Backspace', onBackspace);
        view.off('shortcut:Mod+Backspace', onBackspace);
        view.off('shortcut:Delete', onDelete);
        view.off('shortcut:Alt+Delete', onDelete);
        view.off('shortcut:Tab', onTab);
        view.off('shortcut:Shift+Tab', onTab);
        view.root.removeEventListener('paste', onPaste);
      }
    }
  }
}

<commit_msg>Fix active formats to persist after new lines<commit_after>import { getNodeIndex } from '../view/selection';
import { deltaFromDom } from '../view/dom';
import diff from '../diff';

const SOURCE_USER = 'user';
const lastWord = /\w+[^\w]*$/;
const firstWord = /^[^\w]*\w+/;
const lastLine = /[^\n]*$/;
const firstLine = /^[^\n]*/;
let pasteMutation = false;

// Basic text input module. Prevent any actions other than typing characters and handle with the API.
export default function input() {
  return view => {
    const editor = view.editor;
    const mutationOptions = {
      characterData: true,
      characterDataOldValue: true,
      subtree: true,
      childList: true,
      attributes: true
    };

    // Detects changes from spell-check and the user typing
    function onMutate(list) {
      list = list.filter(m => m.target !== view.root);
      if (!list.length) return;

      const selection = view.getSelection();
      const textChange = getTextChange(list);

      // Only one text node has been altered. Optimize for view most common case, which is typing in a paragraph.
      if (textChange) {
        if (textChange.ops.length) {
          // console.log('changing a little', textChange);
          if (!editor.updateContents(textChange, SOURCE_USER, selection)) {
            view.render();
          }
        }
      } else {
        let contents = deltaFromDom(view, view.root, { ignoreAttributes: true });
        if (pasteMutation) {
          const from = editor.selection ? editor.selection[0] : undefined;
          const to = selection ? selection[1] : undefined;
          contents = contents.clean(from, to);
        }
        contents = contents.compose(view.reverseDecorators);
        // Decorators may extend beyond the text, pull those out
        contents.ops = contents.ops.filter(op => op.insert);
        const change = editor.contents.diff(contents);
        // console.log('changing a lot (possibly)', change);
        if (!editor.updateContents(change, SOURCE_USER, selection)) {
          view.render();
        }
      }
    }

    function onEnter(event, shortcut) {
      if (event.defaultPrevented) return;
      event.preventDefault();
      let [ from, to ] = editor.getSelectedRange();

      const line = editor.contents.getLine(from);
      let attributes = line.attributes;
      const block = view.paper.blocks.find(attributes);
      const isDefault = !block || block === view.paper.blocks.getDefault();
      const length = line.end - line.start - 1;
      const atEnd = to === line.end - 1;
      if (atEnd && !isDefault && block.defaultFollows) {
        attributes = {};
      }
      const activeFormats = editor.activeFormats;
      if (!length && !isDefault && !block.defaultFollows && from === to) {
        editor.formatLine(from, to, {}, SOURCE_USER);
      } else {
        let selection = from + 1;
        if (from === to && atEnd) {
          from++;
          to++;
        }
        editor.insertText(from, to, '\n', attributes, SOURCE_USER, selection);
      }
      editor.activeFormats = activeFormats;
    }

    function onShiftEnter(event) {
      if (event.defaultPrevented) return;
      event.preventDefault();
      let [ from, to ] = editor.getSelectedRange();
      editor.insertEmbed(from, to, 'br', true, null, SOURCE_USER);
    }

    function onBackspace(event, shortcut) {
      if (event.defaultPrevented || shortcut === 'Alt+Backspace' || shortcut === 'Mod+Backspace') return;
      event.preventDefault();

      let [ from, to ] = editor.getSelectedRange();
      if (from + to === 0) {
        const line = editor.contents.getLine(from);
        const block = view.paper.blocks.find(line.attributes);
        if (block) editor.formatLine(0, {}, SOURCE_USER);
      } else {
        // The "from" block needs to stay the same. The "to" block gets merged into it
        if (from === to) {
          if ((shortcut === 'Alt+Backspace' && view.isMac) || (shortcut === 'Mod+Backspace' && !view.isMac)) {
            const match = editor.getText().slice(0, from).match(lastWord);
            if (match) from -= match[0].length;
          } else if (shortcut === 'Mod+Backspace' && view.isMac) {
            const match = editor.getText().slice(0, from).match(lastLine);
            if (match) from -= match[0].length;
          } else {
            const line = editor.contents.getLine(from);
            if (from === line.start) {
              const blocks = view.paper.blocks;
              const block = blocks.find(line.attributes);
              if (block && block !== blocks.getDefault() && !block.defaultFollows) {
                editor.formatLine(from, {}, SOURCE_USER);
                return;
              }
            }

            from--;
          }
        }
        editor.deleteText(from, to, SOURCE_USER);
      }
    }

    function onDelete(event, shortcut) {
      if (event.defaultPrevented || shortcut === 'Alt+Delete') return;
      event.preventDefault();

      let [ from, to ] = editor.getSelectedRange();
      if (from === to && from === editor.length) return;

      if (from === to) {
        if (shortcut === 'Alt+Delete' && view.isMac) {
          const match = editor.getText().slice(from).match(firstWord);
          if (match) to += match[0].length;
        } else {
          to++;
        }
      }
      editor.deleteText(from, to, SOURCE_USER);
    }

    function onTab(event, shortcut) {
      if (event.defaultPrevented) return;
      event.preventDefault();

      const direction = shortcut === 'Tab' || shortcut === 'Mod+]' ? 1 : -1;
      const [ from, to ] = editor.getSelectedRange();
      const lines = editor.contents.getLines(from, to);

      editor.transaction(() => {
        lines.forEach((line, i) => {
          if (line.attributes.list) {
            let prevLine = lines[i -1];
            if (!prevLine && line.index > 0) prevLine = editor.contents.getLine(line.start - 1);
            const prevIndent = prevLine && prevLine.attributes.list ? prevLine.attributes.indent || 0 : -1;

            let indent = line.attributes.indent || 0;
            indent += direction;
            if (indent > prevIndent + 1) return console.log('will not indent too much');
            if (indent < 0) {
              editor.formatLine(line.start, {});
            } else {
              const attributes = { ...line.attributes, indent };
              editor.formatLine(line.start, attributes);
            }
          }
        });
      }, SOURCE_USER);
    }

    function getTextChange(mutations) {
      const mutation = getTextChangeMutation(mutations);
      if (!mutation) return;

      const change = editor.delta();
      const node = mutation.type === 'characterData' ? mutation.target : mutation.addedNodes[0];
      const index = view.reverseDecorators.transform(getNodeIndex(view, node));
      change.retain(index);

      if (mutation.type === 'characterData') {
        const diffs = diff(mutation.oldValue.replace(/\xA0/g, ' '), mutation.target.nodeValue.replace(/\xA0/g, ' '));
        diffs.forEach(([ action, string ]) => {
          if (action === diff.EQUAL) change.retain(string.length);
          else if (action === diff.DELETE) change.delete(string.length);
          else if (action === diff.INSERT) {
            change.insert(string, editor.activeFormats);
          }
        });
        change.chop();
      } else {
        change.insert(node.nodeValue.replace(/\xA0/g, ' '), editor.activeFormats);
      }

      return change;
    }

    function getTextChangeMutation(mutations) {
      const seen = new Set();
      mutations = mutations.filter(m => {
        if (m.type !== 'characterData') return true;
        if (seen.has(m.target)) return false;
        seen.add(m.target);
        return true;
      });

      if (mutations.length > 3) return;

      const first = mutations[0];
      const second = mutations[1];
      const third = mutations[2];
      const added = first.addedNodes[0];
      const removed = second && second.removedNodes[0];

      if (mutations.length === 1) {
        if (first.type === 'characterData') {
          return first;
        } else if (first.type === 'childList' && added && added.nodeType === Node.TEXT_NODE) {
          return first;
        }
      } else {
        if (first.type === 'childList' && added && added.nodeType === Node.TEXT_NODE) {
          const textNode = third || second;
          if (removed && removed.nodeName !== 'BR') return;
          if (textNode.type === 'characterData' && textNode.target === added) {
            return first;
          }
        }
      }
    }

    function onPaste() {
      pasteMutation = true;
      setTimeout(() => {
        pasteMutation = false;
      }, 50);
    }

    const observer = new MutationObserver(onMutate);
    observer.observe(view.root, mutationOptions);

    // Don't observe the changes that occur when the view updates, we only want to respond to changes that happen
    // outside of our API to read them back in
    function onRendering() {
      observer.disconnect();
    }

    // Once the view update is complete, continue observing for changes
    function onRender() {
      observer.observe(view.root, mutationOptions);
    }

    view.on('rendering', onRendering);
    view.on('render', onRender);
    view.on('shortcut:Enter', onEnter);
    view.on('shortcut:Shift+Enter', onShiftEnter);
    view.on('shortcut:Backspace', onBackspace);
    view.on('shortcut:Alt+Backspace', onBackspace);
    view.on('shortcut:Mod+Backspace', onBackspace);
    view.on('shortcut:Delete', onDelete);
    view.on('shortcut:Alt+Delete', onDelete);
    view.on('shortcut:Tab', onTab);
    view.on('shortcut:Shift+Tab', onTab);
    view.root.addEventListener('paste', onPaste);

    return {
      destroy() {
        observer.disconnect();
        view.off('rendering', onRendering);
        view.off('render', onRender);
        view.off('shortcut:Enter', onEnter);
        view.off('shortcut:Shift+Enter', onShiftEnter);
        view.off('shortcut:Backspace', onBackspace);
        view.off('shortcut:Alt+Backspace', onBackspace);
        view.off('shortcut:Mod+Backspace', onBackspace);
        view.off('shortcut:Delete', onDelete);
        view.off('shortcut:Alt+Delete', onDelete);
        view.off('shortcut:Tab', onTab);
        view.off('shortcut:Shift+Tab', onTab);
        view.root.removeEventListener('paste', onPaste);
      }
    }
  }
}

<|endoftext|>


<commit_before>(function(window, _, Backbone) {
    'use strict';
    var Monaco = window.Monaco = (window.Monaco || {});

    /* -- ROUTER ----------------------------------------------------------- */
    // Cached regular expressions for matching named param parts and splatted
    // parts of route strings.
    var optionalParam = /\((.*?)\)/g;
    var namedParam    = /(\(\?)?:\w+/g;
    var splatParam    = /\*\w+/g;
    var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;

    // Monaco.Router.prototype = _.extend(Monaco.Router.prototype, {
    _.extend(Monaco.Router.prototype, {
        // return the regexp option of the specific router if available
        // otherwise undefined will be returned
        _routeConstraints : function(routeKey) {
            // var value = this._routes[route];
            var route = _.find(this._routes, function(route) {
                return route.key === routeKey;
            });
            if (route && route.value && _.isArray(route.value) && route.value.length > 1 && _.isObject(route.value[1])) {
                return route.value[1].regexp;
            }
            return void 0;
        },

        // override the original Backbone method to deal with the regex constraints
        _routeToRegExp : function(route) {
            var constraints = this._routeConstraints(route);
            route = route.replace(escapeRegExp, '\\$&')
                         .replace(optionalParam, '(?:$1)?')
                         .replace(namedParam, function(match, optional){
                            // this `if` is the only difference from the original method from Backbone
                            if (constraints && constraints[match.substr(1)]) {
                                var reStr = constraints[match.substr(1)].toString();
                                return '(' + reStr.slice(1, reStr.lastIndexOf('/')) + ')';
                            }
                            return optional ? match : '([^\/]+)';
                         })
                         .replace(splatParam, '(.*?)');
            return new RegExp('^' + route + '$');
        },

        filter : function(filters, controller) {
            for (var i = (filters.length - 1) , l = 0; i >= l; i-- ) {
                var next = controller;
                controller = this._wrapFilter(filters[i], next);
            }

            return controller;
        },

        // internal list of filters
        _filters : {},

        _wrapFilter: function(filter, next) {
            return _.bind(function() {
                this._filters[filter].call(this, next, arguments);
            }, this);
        },

        // adds a filter method to list of filters
        addFilter : function(name, callback) {
            if (this._filters[name]) {
                throw new Error('This filter already exists: ' + name);
            }
            this._filters[name] = callback;
        },

        // returns the original route definition based on the url name informed
        _getByName: function(name) {
            var possibleRoutes = _.map(this._routes, function(options, route) {
                if (_.isArray(options) && options.length > 1) {
                    if ((_.isString(options[1]) && options[1] === name) ||
                        (_.isObject(options[1]) && options.name === name)) {
                        return route;
                    }
                }
                return null;
            });

            route = _.compact(possibleRoutes);
            if (route.length > 0) {
                return route[0];
            }
            return false;
        },

        // returns a reverse url based on the url name and parameters passed to it.
        // This url won't be validated against the regexp constraints even if available.
        // Neither it will validate if you provided enough parameters, the missing
        // parameters will be displayed as the original url definition.
        reverse: function(name, params) {
            var url = '';
            params = params || {};
            if (!name) {
                throw new Error('required `name` parameter for `reverse` method');
            }

            url = this._getByName(name);
            _.each(params, function(value, key) {
                url = url.replace(':'+key, value);
            });

            return url;
        }
    });
}(window, window._, window.Backbone));
<commit_msg>modified monaco router code formating<commit_after>(function(window, _, Backbone) {
    'use strict';
    var Monaco = window.Monaco = (window.Monaco || {});

    /* -- ROUTER ----------------------------------------------------------- */
    // Cached regular expressions for matching named param parts and splatted
    // parts of route strings.
    var optionalParam = /\((.*?)\)/g;
    var namedParam    = /(\(\?)?:\w+/g;
    var splatParam    = /\*\w+/g;
    var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;

    // return the regexp option of the specific router if available
    // otherwise undefined will be returned
    Monaco.Router.prototype._routeConstraints = function(routeKey) {
        // var value = this._routes[route];
        var route = _.find(this._routes, function(route) {
            return route.key === routeKey;
        });
        if (route && route.value && _.isArray(route.value) && route.value.length > 1 && _.isObject(route.value[1])) {
            return route.value[1].regexp;
        }
        return void 0;
    };

    // override the original Backbone method to deal with the regex constraints
    Monaco.Router.prototype._routeToRegExp = function(route) {
        var constraints = this._routeConstraints(route);
        route = route.replace(escapeRegExp, '\\$&')
                     .replace(optionalParam, '(?:$1)?')
                     .replace(namedParam, function(match, optional){
                        // this `if` is the only difference from the original method from Backbone
                        if (constraints && constraints[match.substr(1)]) {
                            var reStr = constraints[match.substr(1)].toString();
                            return '(' + reStr.slice(1, reStr.lastIndexOf('/')) + ')';
                        }
                        return optional ? match : '([^\/]+)';
                     })
                     .replace(splatParam, '(.*?)');
        return new RegExp('^' + route + '$');
    };

    Monaco.Router.prototype.filter = function(filters, controller) {
        for (var i = (filters.length - 1) , l = 0; i >= l; i-- ) {
            var next = controller;
            controller = this._wrapFilter(filters[i], next);
        }

        return controller;
    };

    // internal list of filters
    Monaco.Router.prototype._filters = {};

    Monaco.Router.prototype._wrapFilter = function(filter, next) {
        return _.bind(function() {
            this._filters[filter].call(this, next, arguments);
        }, this);
    };

    // adds a filter method to list of filters
    Monaco.Router.prototype.addFilter = function(name, callback) {
        if (this._filters[name]) {
            throw new Error('This filter already exists: ' + name);
        }
        this._filters[name] = callback;
    };

    // returns the original route definition based on the url name informed
    Monaco.Router.prototype._getByName = function(name) {
        var possibleRoutes = _.map(this._routes, function(options, route) {
            if (_.isArray(options) && options.length > 1) {
                if ((_.isString(options[1]) && options[1] === name) ||
                    (_.isObject(options[1]) && options.name === name)) {
                    return route;
                }
            }
            return null;
        });

        var route = _.compact(possibleRoutes);
        if (route.length > 0) {
            return route[0];
        }
        return false;
    };

    // returns a reverse url based on the url name and parameters passed to it.
    // This url won't be validated against the regexp constraints even if available.
    // Neither it will validate if you provided enough parameters, the missing
    // parameters will be displayed as the original url definition.
    Monaco.Router.prototype.reverse = function(name, params) {
        var url = '';
        params = params || {};
        if (!name) {
            throw new Error('required `name` parameter for `reverse` method');
        }

        url = this._getByName(name);
        _.each(params, function(value, key) {
            url = url.replace(':'+key, value);
        });

        return url;
    };
}(window, window._, window.Backbone));
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { push as pushAction } from 'react-router-redux';
import { Card, CardTitle } from 'material-ui/Card';
import inflection from 'inflection';
import { change as changeFormValueAction, getFormValues } from 'redux-form';
import debounce from 'lodash.debounce';
import queryReducer, { SET_SORT, SET_PAGE, SET_FILTER, SORT_DESC } from '../../reducer/resource/list/queryReducer';
import Title from '../layout/Title';
import DefaultPagination from './Pagination';
import DefaultActions from './Actions';
import { crudGetList as crudGetListAction } from '../../actions/dataActions';
import { changeListParams as changeListParamsAction } from '../../actions/listActions';

const filterFormName = 'filterForm';

/**
 * List page component
 *
 * The <List> component renders the list layout (title, buttons, filters, pagination),
 * and fetches the list of records from the REST API.
 * It then delegates the rendering of the list of records to its child component.
 * Usually, it's a <Datagrid>, responsible for displaying a table with one row for each post.
 *
 * In Redux terms, <List> is a connected component, and <Datagrid> is a dumb component.
 *
 * Props:
 *   - title
 *   - perPage
 *   - defaultSort
 *   - actions
 *   - filter
 *   - pagination
 *
 * @example
 *     const PostFilter = (props) => (
 *         <Filter {...props}>
 *             <TextInput label="Search" source="q" alwaysOn />
 *             <TextInput label="Title" source="title" />
 *         </Filter>
 *     );
 *     export const PostList = (props) => (
 *         <List {...props} title="List of posts" filter={<PostFilter />} defaultSort={{ field: 'published_at' }}>
 *             <Datagrid>
 *                 <TextField source="id" />
 *                 <TextField source="title" />
 *                 <DateField source="published_at" style={{ fontStyle: 'italic' }} />
 *                 <EditButton />
 *             </Datagrid>
 *         </List>
 *     );
 */
export class List extends Component {
    constructor(props) {
        super(props);
        this.debouncedSetFilters = debounce(this.setFilters.bind(this), 500);
        this.state = {};
    }

    componentDidMount() {
        this.updateData();
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.resource !== this.props.resource
         || nextProps.query.sort !== this.props.query.sort
         || nextProps.query.order !== this.props.query.order
         || nextProps.query.page !== this.props.query.page
         || nextProps.query.filter !== this.props.query.filter) {
            this.updateData(Object.keys(nextProps.query).length > 0 ? nextProps.query : nextProps.params);
        }
        if (Object.keys(nextProps.filters).length === 0 && Object.keys(this.props.filters).length === 0) {
            return;
        }
        if (nextProps.filters !== this.props.filters) {
            const nextFilters = nextProps.filters;
            Object.keys(nextFilters).forEach(filterName => {
                if (nextFilters[filterName] === '') {
                    // remove empty filter from query
                    delete nextFilters[filterName];
                }
            });
            this.debouncedSetFilters(nextFilters);
        }
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.isLoading === this.props.isLoading && nextState === this.state) {
            return false;
        }
        return true;
    }

    componentWillUnmount() {
        this.debouncedSetFilters.cancel();
    }

    getBasePath() {
        return this.props.location.pathname;
    }

    refresh = (event) => {
        event.stopPropagation();
        this.updateData();
    }

    /**
     * Merge list params from 3 different sources:
     *   - the query string
     *   - the params stored in the state (from previous navigation)
     *   - the props passed to the List component
     */
    getQuery() {
        const query = Object.keys(this.props.query).length > 0 ? this.props.query : { ...this.props.params };
        if (!query.sort) {
            query.sort = this.props.defaultSort.field;
            query.order = this.props.defaultSort.order;
        }
        if (!query.perPage) {
            query.perPage = this.props.perPage;
        }
        return query;
    }

    updateData(query) {
        const params = query || this.getQuery();
        const { sort, order, page, perPage, filter } = params;
        this.props.crudGetList(this.props.resource, { page, perPage }, { field: sort, order }, filter);
    }

    setSort = sort => this.changeParams({ type: SET_SORT, payload: sort });

    setPage = page => this.changeParams({ type: SET_PAGE, payload: page });

    setFilters = filters => this.changeParams({ type: SET_FILTER, payload: filters });

    showFilter = filterName => this.setState({ [filterName]: true });

    hideFilter = (filterName) => {
        this.setState({ [filterName]: false });
        this.props.changeFormValue(filterFormName, filterName, '');
        this.setFilters({ ...this.props.filters, [filterName]: undefined });
    }

    changeParams(action) {
        const newParams = queryReducer(this.getQuery(), action);
        this.props.push({ ...this.props.location, query: { ...newParams, filter: JSON.stringify(newParams.filter) } });
        this.props.changeListParams(this.props.resource, newParams);
    }

    render() {
        const { filter, pagination = <DefaultPagination />, actions = <DefaultActions />, resource, hasCreate, title, data, ids, total, children, isLoading } = this.props;
        const query = this.getQuery();
        const filterValues = query.filter;
        const basePath = this.getBasePath();
        return (
            <Card style={{ margin: '2em', opacity: isLoading ? 0.8 : 1 }}>
                {actions && React.cloneElement(actions, {
                    resource,
                    filter,
                    filterValues,
                    basePath,
                    hasCreate,
                    displayedFilters: this.state,
                    showFilter: this.showFilter,
                    refresh: this.refresh,
                })}
                <CardTitle title={<Title title={title} defaultTitle={`${inflection.humanize(inflection.pluralize(resource))} List`} />} />
                {filter && React.cloneElement(filter, {
                    resource,
                    hideFilter: this.hideFilter,
                    filterValues,
                    displayedFilters: this.state,
                    context: 'form',
                })}
                {React.cloneElement(children, {
                    resource,
                    ids,
                    data,
                    currentSort: { field: query.sort, order: query.order },
                    basePath,
                    setSort: this.setSort,
                })}
                {pagination && React.cloneElement(pagination, {
                    total,
                    page: parseInt(query.page, 10),
                    perPage: parseInt(query.perPage, 10),
                    setPage: this.setPage,
                })}
            </Card>
        );
    }
}

List.propTypes = {
    // the props you can change
    title: PropTypes.any,
    filter: PropTypes.element,
    pagination: PropTypes.element,
    actions: PropTypes.element,
    perPage: PropTypes.number.isRequired,
    defaultSort: PropTypes.shape({
        field: PropTypes.string,
        order: PropTypes.string,
    }),
    children: PropTypes.element.isRequired,
    // the props managed by admin-on-rest
    changeFormValue: PropTypes.func.isRequired,
    changeListParams: PropTypes.func.isRequired,
    crudGetList: PropTypes.func.isRequired,
    data: PropTypes.object, // eslint-disable-line react/forbid-prop-types
    filters: PropTypes.object, // eslint-disable-line react/forbid-prop-types
    hasCreate: PropTypes.bool.isRequired,
    hasEdit: PropTypes.bool.isRequired,
    ids: PropTypes.array,
    isLoading: PropTypes.bool.isRequired,
    location: PropTypes.object.isRequired,
    path: PropTypes.string,
    params: PropTypes.object.isRequired,
    push: PropTypes.func.isRequired,
    query: PropTypes.object.isRequired,
    resource: PropTypes.string.isRequired,
    total: PropTypes.number.isRequired,
};

List.defaultProps = {
    filters: {},
    perPage: 10,
    defaultSort: {
        field: 'id',
        order: SORT_DESC,
    },
};

function mapStateToProps(state, props) {
    const resourceState = state.admin[props.resource];
    const query = props.location.query;
    if (query.filter && typeof query.filter === 'string') {
        // if the List has no filter component, the filter is always "{}"
        // avoid deserialization and keep identity by using a constant
        query.filter = props.filter ? JSON.parse(query.filter) : resourceState.list.params.filter;
    }

    return {
        query,
        params: resourceState.list.params,
        ids: resourceState.list.ids,
        total: resourceState.list.total,
        data: resourceState.data,
        isLoading: state.admin.loading > 0,
        filters: props.filter ? getFormValues(filterFormName)(state) : resourceState.list.params.filter,
    };
}

export default connect(
    mapStateToProps,
    {
        crudGetList: crudGetListAction,
        changeFormValue: changeFormValueAction,
        changeListParams: changeListParamsAction,
        push: pushAction,
    },
)(List);
<commit_msg>Fix default list state<commit_after>import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { push as pushAction } from 'react-router-redux';
import { Card, CardTitle } from 'material-ui/Card';
import inflection from 'inflection';
import { change as changeFormValueAction, getFormValues } from 'redux-form';
import debounce from 'lodash.debounce';
import queryReducer, { SET_SORT, SET_PAGE, SET_FILTER, SORT_DESC } from '../../reducer/resource/list/queryReducer';
import Title from '../layout/Title';
import DefaultPagination from './Pagination';
import DefaultActions from './Actions';
import { crudGetList as crudGetListAction } from '../../actions/dataActions';
import { changeListParams as changeListParamsAction } from '../../actions/listActions';

const filterFormName = 'filterForm';

/**
 * List page component
 *
 * The <List> component renders the list layout (title, buttons, filters, pagination),
 * and fetches the list of records from the REST API.
 * It then delegates the rendering of the list of records to its child component.
 * Usually, it's a <Datagrid>, responsible for displaying a table with one row for each post.
 *
 * In Redux terms, <List> is a connected component, and <Datagrid> is a dumb component.
 *
 * Props:
 *   - title
 *   - perPage
 *   - defaultSort
 *   - actions
 *   - filter
 *   - pagination
 *
 * @example
 *     const PostFilter = (props) => (
 *         <Filter {...props}>
 *             <TextInput label="Search" source="q" alwaysOn />
 *             <TextInput label="Title" source="title" />
 *         </Filter>
 *     );
 *     export const PostList = (props) => (
 *         <List {...props} title="List of posts" filter={<PostFilter />} defaultSort={{ field: 'published_at' }}>
 *             <Datagrid>
 *                 <TextField source="id" />
 *                 <TextField source="title" />
 *                 <DateField source="published_at" style={{ fontStyle: 'italic' }} />
 *                 <EditButton />
 *             </Datagrid>
 *         </List>
 *     );
 */
export class List extends Component {
    constructor(props) {
        super(props);
        this.debouncedSetFilters = debounce(this.setFilters.bind(this), 500);
        this.state = {};
    }

    componentDidMount() {
        this.updateData();
        if (Object.keys(this.props.query).length > 0) {
             this.props.changeListParams(this.props.resource, this.props.query);
        }
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.resource !== this.props.resource
         || nextProps.query.sort !== this.props.query.sort
         || nextProps.query.order !== this.props.query.order
         || nextProps.query.page !== this.props.query.page
         || nextProps.query.filter !== this.props.query.filter) {
            this.updateData(Object.keys(nextProps.query).length > 0 ? nextProps.query : nextProps.params);
        }
        if (Object.keys(nextProps.filters).length === 0 && Object.keys(this.props.filters).length === 0) {
            return;
        }
        if (nextProps.filters !== this.props.filters) {
            const nextFilters = nextProps.filters;
            Object.keys(nextFilters).forEach(filterName => {
                if (nextFilters[filterName] === '') {
                    // remove empty filter from query
                    delete nextFilters[filterName];
                }
            });
            this.debouncedSetFilters(nextFilters);
        }
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.isLoading === this.props.isLoading && nextState === this.state) {
            return false;
        }
        return true;
    }

    componentWillUnmount() {
        this.debouncedSetFilters.cancel();
    }

    getBasePath() {
        return this.props.location.pathname;
    }

    refresh = (event) => {
        event.stopPropagation();
        this.updateData();
    }

    /**
     * Merge list params from 3 different sources:
     *   - the query string
     *   - the params stored in the state (from previous navigation)
     *   - the props passed to the List component
     */
    getQuery() {
        const query = Object.keys(this.props.query).length > 0 ? this.props.query : { ...this.props.params };
        if (!query.sort) {
            query.sort = this.props.defaultSort.field;
            query.order = this.props.defaultSort.order;
        }
        if (!query.perPage) {
            query.perPage = this.props.perPage;
        }
        return query;
    }

    updateData(query) {
        const params = query || this.getQuery();
        const { sort, order, page, perPage, filter } = params;
        this.props.crudGetList(this.props.resource, { page, perPage }, { field: sort, order }, filter);
    }

    setSort = sort => this.changeParams({ type: SET_SORT, payload: sort });

    setPage = page => this.changeParams({ type: SET_PAGE, payload: page });

    setFilters = filters => this.changeParams({ type: SET_FILTER, payload: filters });

    showFilter = filterName => this.setState({ [filterName]: true });

    hideFilter = (filterName) => {
        this.setState({ [filterName]: false });
        this.props.changeFormValue(filterFormName, filterName, '');
        this.setFilters({ ...this.props.filters, [filterName]: undefined });
    }

    changeParams(action) {
        const newParams = queryReducer(this.getQuery(), action);
        this.props.push({ ...this.props.location, query: { ...newParams, filter: JSON.stringify(newParams.filter) } });
        this.props.changeListParams(this.props.resource, newParams);
    }

    render() {
        const { filter, pagination = <DefaultPagination />, actions = <DefaultActions />, resource, hasCreate, title, data, ids, total, children, isLoading } = this.props;
        const query = this.getQuery();
        const filterValues = query.filter;
        const basePath = this.getBasePath();
        return (
            <Card style={{ margin: '2em', opacity: isLoading ? 0.8 : 1 }}>
                {actions && React.cloneElement(actions, {
                    resource,
                    filter,
                    filterValues,
                    basePath,
                    hasCreate,
                    displayedFilters: this.state,
                    showFilter: this.showFilter,
                    refresh: this.refresh,
                })}
                <CardTitle title={<Title title={title} defaultTitle={`${inflection.humanize(inflection.pluralize(resource))} List`} />} />
                {filter && React.cloneElement(filter, {
                    resource,
                    hideFilter: this.hideFilter,
                    filterValues,
                    displayedFilters: this.state,
                    context: 'form',
                })}
                {React.cloneElement(children, {
                    resource,
                    ids,
                    data,
                    currentSort: { field: query.sort, order: query.order },
                    basePath,
                    setSort: this.setSort,
                })}
                {pagination && React.cloneElement(pagination, {
                    total,
                    page: parseInt(query.page, 10),
                    perPage: parseInt(query.perPage, 10),
                    setPage: this.setPage,
                })}
            </Card>
        );
    }
}

List.propTypes = {
    // the props you can change
    title: PropTypes.any,
    filter: PropTypes.element,
    pagination: PropTypes.element,
    actions: PropTypes.element,
    perPage: PropTypes.number.isRequired,
    defaultSort: PropTypes.shape({
        field: PropTypes.string,
        order: PropTypes.string,
    }),
    children: PropTypes.element.isRequired,
    // the props managed by admin-on-rest
    changeFormValue: PropTypes.func.isRequired,
    changeListParams: PropTypes.func.isRequired,
    crudGetList: PropTypes.func.isRequired,
    data: PropTypes.object, // eslint-disable-line react/forbid-prop-types
    filters: PropTypes.object, // eslint-disable-line react/forbid-prop-types
    hasCreate: PropTypes.bool.isRequired,
    hasEdit: PropTypes.bool.isRequired,
    ids: PropTypes.array,
    isLoading: PropTypes.bool.isRequired,
    location: PropTypes.object.isRequired,
    path: PropTypes.string,
    params: PropTypes.object.isRequired,
    push: PropTypes.func.isRequired,
    query: PropTypes.object.isRequired,
    resource: PropTypes.string.isRequired,
    total: PropTypes.number.isRequired,
};

List.defaultProps = {
    filters: {},
    perPage: 10,
    defaultSort: {
        field: 'id',
        order: SORT_DESC,
    },
};

function mapStateToProps(state, props) {
    const resourceState = state.admin[props.resource];
    const query = props.location.query;
    if (query.filter && typeof query.filter === 'string') {
        // if the List has no filter component, the filter is always "{}"
        // avoid deserialization and keep identity by using a constant
        query.filter = props.filter ? JSON.parse(query.filter) : resourceState.list.params.filter;
    }

    return {
        query,
        params: resourceState.list.params,
        ids: resourceState.list.ids,
        total: resourceState.list.total,
        data: resourceState.data,
        isLoading: state.admin.loading > 0,
        filters: props.filter ? getFormValues(filterFormName)(state) : resourceState.list.params.filter,
    };
}

export default connect(
    mapStateToProps,
    {
        crudGetList: crudGetListAction,
        changeFormValue: changeFormValueAction,
        changeListParams: changeListParamsAction,
        push: pushAction,
    },
)(List);
<|endoftext|>