<commit_before>import Ember from 'ember';
import TaskInstance from './-task-instance';
import { _cleanupOnDestroy } from './utils';
import {
  enqueueTasksPolicy,
  dropQueuedTasksPolicy,
  cancelOngoingTasksPolicy,
  dropButKeepLatestPolicy
} from './-buffer-policy';

const ComputedProperty = Ember.__loader.require("ember-metal/computed").ComputedProperty;
const { computed } = Ember;

function isSuccess(nextPerformState) {
  return nextPerformState === 'succeed' || nextPerformState === 'cancel_previous';
}

/**
  The `Task` object lives on a host Ember object (e.g.
  a Component, Route, or Controller). You call the
  {@linkcode Task#perform .perform()} method on this object
  to create run individual {@linkcode TaskInstance}s,
  and at any point, you can call the {@linkcode Task#cancelAll .cancelAll()}
  method on this object to cancel all running or enqueued
  {@linkcode TaskInstance}s.

  <style>
    .ignore-this--this-is-here-to-hide-constructor,
    #Task{ display: none }
  </style>

  @class Task
*/
export const Task = Ember.Object.extend({
  fn: null,
  context: null,
  bufferPolicy: null,
  isIdle: computed.equal('concurrency', 0),
  isRunning: Ember.computed.not('isIdle'),
  state: computed('isIdle', function() {
    return this.get('isIdle') ? 'idle' : 'running';
  }),
  _maxConcurrency: Infinity,
  _activeTaskInstances: null,
  _needsFlush: null,
  _propertyName: null,
  _origin: null,

  name: computed.oneWay('_propertyName'),

  _performsPath: null,
  _performs: computed('_performsPath', function() {
    let path = this.get('_performsPath');
    if (!path) { return; }

    let task = this.context.get(path);
    if (!(task instanceof Task)) {
      throw new Error(`You wrote .performs('${path}'), but the object at '${path}' is not a Task`);
    }
    return task;
  }),

  _performsState: computed('_performs.nextPerformState', function() {
    return this.get('_performs.nextPerformState') || 'succeed';
  }),

  /**
   * EXPERIMENTAL
   *
   * This value describes what would happen to the TaskInstance returned
   * from .perform() if .perform() were called right now.  Returns one of
   * the following values:
   *
   * - `succeed`: new TaskInstance will start running immediately
   * - `drop`: new TaskInstance will be dropped
   * - `enqueue`: new TaskInstance will be enqueued for later execution
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  nextPerformState: computed('_performsState', function() {
    let performsState = this.get('_performsState');
    return isSuccess(performsState) ?
      this.bufferPolicy.getNextPerformStatus(this) :
      performsState;
  }),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would immediately start running
   * the returned TaskInstance.
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillSucceed: computed('nextPerformState', function() {
    return isSuccess(this.get('nextPerformState'));
  }),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would immediately cancel (drop)
   * the returned TaskInstance.
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillDrop: computed.equal('nextPerformState', 'drop'),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would enqueue the TaskInstance
   * rather than execute immediately.
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillEnqueue: computed.equal('nextPerformState', 'enqueue'),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would cause a previous task to be canceled
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillCancelPrevious: computed.equal('nextPerformState', 'cancel_previous'),

  init() {
    this._super(...arguments);
    this._activeTaskInstances = Ember.A();
    this._queuedTaskInstances = Ember.A();

    let self = this;
    this.perform = function(...args) {
      if (this !== self) {
        console.warn(`The use of ${self._propertyName}.perform within a template is deprecated and won't be supported in future versions of ember-concurrency. Please use the \`perform\` helper instead, e.g. {{perform ${self._propertyName}}}`);
      }
      return self._perform(...args);
    };

    this._needsFlush = Ember.run.bind(this, this._scheduleFlush);

    _cleanupOnDestroy(this.context, this, 'cancelAll');
  },

  /**
   * The current number of active running task instances. This
   * number will never exceed maxConcurrency.
   *
   * @memberof Task
   * @instance
   * @readOnly
   */
  concurrency: 0,

  /**
   * Creates a new {@linkcode TaskInstance} and attempts to run it right away.
   * If running this task instance would increase the task's concurrency
   * to a number greater than the task's maxConcurrency, this task
   * instance might be immediately canceled (dropped), or enqueued
   * to run at later time, after the currently running task(s) have finished.
   *
   * @method perform
   * @memberof Task
   * @param {*} arg* - args to pass to the task function
   * @instance
   */
  perform: null,

  /**
   * Cancels all running or queued `TaskInstance`s for this Task.
   * If you're trying to cancel a specific TaskInstance (rather
   * than all of the instances running under this task) call
   * `.cancel()` on the specific TaskInstance.
   *
   * @method cancelAll
   * @memberof Task
   * @instance
   */
  cancelAll() {
    this.spliceTaskInstances(this._activeTaskInstances, 0, this._activeTaskInstances.length);
    this.spliceTaskInstances(this._queuedTaskInstances, 0, this._queuedTaskInstances.length);
  },

  spliceTaskInstances(taskInstances, index, count) {
    for (let i = index; i < index + count; ++i) {
      taskInstances[i].cancel();
    }
    taskInstances.splice(index, count);
  },

  toString() {
    return `<Task:${Ember.guidFor(this)}> of ${this._origin}`;
  },

  _perform(...args) {
    let taskInstance = TaskInstance.create({
      fn: this.fn,
      args,
      context: this.context,
      task: this,
      _origin: this,
    });

    if (this.get('_performs') && !this.get('performWillSucceed')) {
      // tasks linked via .performs() should be immediately dropped
      // before they even have a chance to run if we know that
      // .performWillSucceed is false.
      taskInstance.cancel();
      return taskInstance;
    }

    this._queuedTaskInstances.push(taskInstance);
    this._needsFlush();
    this.notifyPropertyChange('nextPerformState');

    return taskInstance;
  },

  _flushScheduled: false,
  _scheduleFlush() {
    this._flushScheduled = true;
    Ember.run.once(this, this._flushQueues);
  },

  _flushQueues() {
    this._flushScheduled = false;
    this._activeTaskInstances = Ember.A(this._activeTaskInstances.filterBy('isFinished', false));

    this.bufferPolicy.schedule(this);

    for (let i = 0; i < this._activeTaskInstances.length; ++i) {
      let taskInstance = this._activeTaskInstances[i];
      if (!taskInstance.hasStarted) {
        // use internal promise so that it doesn't cancel error reporting
        taskInstance._start()._defer.promise.then(this._needsFlush, this._needsFlush);
      }
    }

    this.notifyPropertyChange('nextPerformState');

    let concurrency = this._activeTaskInstances.length;
    this.set('concurrency', concurrency);
    if (this._completionDefer && concurrency === 0) {
      this._completionDefer.resolve();
      this._completionDefer = null;
    }
  },

  _completionDefer: null,
  _getCompletionPromise() {
    return new Ember.RSVP.Promise(r => {
      Ember.run.schedule('actions', null, () => {
        let defer = Ember.RSVP.defer();
        if (!this._flushScheduled &&
            this._activeTaskInstances.length === 0 &&
            this._queuedTaskInstances.length === 0) {
          defer.resolve();
        } else {
          this._completionDefer = defer;
        }
        defer.promise.then(r);
      });
    });
  },
});

/**
  A {@link TaskProperty} is the Computed Property-like object returned
  from the {@linkcode task} function. You can call Task Modifier methods
  on this object to configure the behavior of the {@link Task}.

  See [Managing Task Concurrency](/#/docs/task-concurrency) for an
  overview of all the different task modifiers you can use and how
  they impact automatic cancelation / enqueueing of task instances.

  <style>
    .ignore-this--this-is-here-to-hide-constructor,
    #TaskProperty { display: none }
  </style>

  @class TaskProperty
*/
export function TaskProperty(taskFn) {
  let tp = this;
  ComputedProperty.call(this, function(_propertyName) {
    return Task.create({
      fn: taskFn,
      context: this,
      _origin: this,
      bufferPolicy: tp.bufferPolicy,
      _maxConcurrency: tp._maxConcurrency,
      _performsPath: tp._performsPath && tp._performsPath[0],
      _propertyName,
      _debugName: "",
    });
  });

  this.bufferPolicy = enqueueTasksPolicy;
  this._maxConcurrency = Infinity;
  this.eventNames = null;
  this.cancelEventNames = null;
  this._performsPath = null;
}

TaskProperty.prototype = Object.create(ComputedProperty.prototype);
TaskProperty.prototype.constructor = TaskProperty;
TaskProperty.prototype.setup = function(proto, keyname) {
  addListenersToPrototype(proto, this.eventNames, keyname, '_perform');
  addListenersToPrototype(proto, this.cancelEventNames, keyname, 'cancelAll');
};

/**
 * Calling `task(...).on(eventName)` configures the task to be
 * automatically performed when the specified events fire. In
 * this way, it behaves like
 * [Ember.on](http://emberjs.com/api/classes/Ember.html#method_on).
 *
 * You can use `task(...).on('init')` to perform the task
 * when the host object is initialized.
 *
 * ```js
 * export default Ember.Component.extend({
 *   pollForUpdates: task(function * () {
 *     // ... this runs when the Component is first created
 *     // because we specified .on('init')
 *   }).on('init'),
 *
 *   handleFoo: task(function * (a, b, c) {
 *     // this gets performed automatically if the 'foo'
 *     // event fires on this Component,
 *     // e.g., if someone called component.trigger('foo')
 *   }).on('foo'),
 * });
 * ```
 *
 * [See the Writing Tasks Docs for more info](/#/docs/writing-tasks)
 *
 * @method on
 * @memberof TaskProperty
 * @param {String} eventNames*
 * @instance
 */
TaskProperty.prototype.on = function() {
  this.eventNames = this.eventNames || [];
  this.eventNames.push.apply(this.eventNames, arguments);
  return this;
};

/**
 * This behaves like the {@linkcode TaskProperty#on task(...).on() modifier},
 * but instead will cause the task to be canceled if any of the
 * specified events fire on the parent object.
 *
 * [See the Live Example](/#/docs/examples/route-tasks/1)
 *
 * @method cancelOn
 * @memberof TaskProperty
 * @param {String} eventNames*
 * @instance
 */
TaskProperty.prototype.cancelOn = function() {
  this.cancelEventNames = this.cancelEventNames || [];
  this.cancelEventNames.push.apply(this.cancelEventNames, arguments);
  return this;
};

/**
 * Configures the task to cancel old currently task instances
 * to make room for a new one to perform. Sets default
 * maxConcurrency to 1.
 *
 * [See the Live Example](/#/docs/examples/route-tasks/1)
 *
 * @method restartable
 * @memberof TaskProperty
 * @instance
 */
TaskProperty.prototype.restartable = function() {
  this.bufferPolicy = cancelOngoingTasksPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * Configures the task to run task instances one-at-a-time in
 * the order they were `.perform()`ed. Sets default
 * maxConcurrency to 1.
 *
 * @method enqueue
 * @memberof TaskProperty
 * @instance
 */
TaskProperty.prototype.enqueue = function() {
  this.bufferPolicy = enqueueTasksPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * Configures the task to immediately cancel (i.e. drop) any
 * task instances performed when the task is already running
 * at maxConcurrency. Sets default maxConcurrency to 1.
 *
 * @method drop
 * @memberof TaskProperty
 * @instance
 */
TaskProperty.prototype.drop = function() {
  this.bufferPolicy = dropQueuedTasksPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * @private
 */
TaskProperty.prototype.keepLatest = function() {
  this.bufferPolicy = dropButKeepLatestPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * Sets the maximum number of task instances that are allowed
 * to run at the same time. By default, with no task modifiers
 * applied, this number is Infinity (there is no limit
 * to the number of tasks that can run at the same time).
 * {@linkcode TaskProperty#restartable .restartable()},
 * {@linkcode TaskProperty#enqueue .enqueue()}, and
 * {@linkcode TaskProperty#drop .drop()} set the default
 * maxConcurrency to 1, but you can override this value
 * to set the maximum number of concurrently running tasks
 * to a number greater than 1.
 *
 * [See the AJAX Throttling example](/#/docs/examples/ajax-throttling)
 *
 * The example below uses a task with `maxConcurrency(3)` to limit
 * the number of concurrent AJAX requests (for anyone using this task)
 * to 3.
 *
 * ```js
 * doSomeAjax: task(function * (url) {
 *   return Ember.$.getJSON(url).promise();
 * }).maxConcurrency(3),
 *
 * elsewhere() {
 *   this.get('doSomeAjax').perform("http://www.example.com/json");
 * },
 * ```
 *
 * @method maxConcurrency
 * @memberof TaskProperty
 * @param {Number} n The maximum number of concurrently running tasks
 * @instance
 */
TaskProperty.prototype.maxConcurrency = function(n) {
  this._maxConcurrency = n;
  return this;
};

TaskProperty.prototype._setDefaultMaxConcurrency = function(n) {
  if (this._maxConcurrency === Infinity) {
    this._maxConcurrency = n;
  }
};

TaskProperty.prototype.performs = function() {
  this._performsPath = this._performsPath || [];
  this._performsPath.push.apply(this._performsPath, arguments);
  return this;
};

function addListenersToPrototype(proto, eventNames, taskName, taskMethod) {
  if (eventNames) {
    for (let i = 0; i < eventNames.length; ++i) {
      let eventName = eventNames[i];
      Ember.addListener(proto, eventName, null, makeListener(taskName, taskMethod));
    }
  }
}

function makeListener(taskName, method) {
  return function() {
    let task = this.get(taskName);
    task[method].apply(task, arguments);
  };
}

<commit_msg>document Task.isRunning/isIdle<commit_after>import Ember from 'ember';
import TaskInstance from './-task-instance';
import { _cleanupOnDestroy } from './utils';
import {
  enqueueTasksPolicy,
  dropQueuedTasksPolicy,
  cancelOngoingTasksPolicy,
  dropButKeepLatestPolicy
} from './-buffer-policy';

const ComputedProperty = Ember.__loader.require("ember-metal/computed").ComputedProperty;
const { computed } = Ember;

function isSuccess(nextPerformState) {
  return nextPerformState === 'succeed' || nextPerformState === 'cancel_previous';
}

/**
  The `Task` object lives on a host Ember object (e.g.
  a Component, Route, or Controller). You call the
  {@linkcode Task#perform .perform()} method on this object
  to create run individual {@linkcode TaskInstance}s,
  and at any point, you can call the {@linkcode Task#cancelAll .cancelAll()}
  method on this object to cancel all running or enqueued
  {@linkcode TaskInstance}s.

  <style>
    .ignore-this--this-is-here-to-hide-constructor,
    #Task{ display: none }
  </style>

  @class Task
*/
export const Task = Ember.Object.extend({
  fn: null,
  context: null,
  bufferPolicy: null,

  /**
   * This property is true if this task is NOT running, i.e. the number
   * of currently running TaskInstances is zero.
   *
   * This property is useful for driving the state/style of buttons
   * and loading UI, among other things.
   *
   * @memberof Task
   * @instance
   * @readOnly
   */
  isIdle: computed.equal('concurrency', 0),

  /**
   * This property is true if this task is running, i.e. the number
   * of currently running TaskInstances is greater than zero.
   *
   * This property is useful for driving the state/style of buttons
   * and loading UI, among other things.
   *
   * @memberof Task
   * @instance
   * @readOnly
   */
  isRunning: Ember.computed.not('isIdle'),

  state: computed('isIdle', function() {
    return this.get('isIdle') ? 'idle' : 'running';
  }),
  _maxConcurrency: Infinity,
  _activeTaskInstances: null,
  _needsFlush: null,
  _propertyName: null,
  _origin: null,

  name: computed.oneWay('_propertyName'),

  _performsPath: null,
  _performs: computed('_performsPath', function() {
    let path = this.get('_performsPath');
    if (!path) { return; }

    let task = this.context.get(path);
    if (!(task instanceof Task)) {
      throw new Error(`You wrote .performs('${path}'), but the object at '${path}' is not a Task`);
    }
    return task;
  }),

  _performsState: computed('_performs.nextPerformState', function() {
    return this.get('_performs.nextPerformState') || 'succeed';
  }),

  /**
   * EXPERIMENTAL
   *
   * This value describes what would happen to the TaskInstance returned
   * from .perform() if .perform() were called right now.  Returns one of
   * the following values:
   *
   * - `succeed`: new TaskInstance will start running immediately
   * - `drop`: new TaskInstance will be dropped
   * - `enqueue`: new TaskInstance will be enqueued for later execution
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  nextPerformState: computed('_performsState', function() {
    let performsState = this.get('_performsState');
    return isSuccess(performsState) ?
      this.bufferPolicy.getNextPerformStatus(this) :
      performsState;
  }),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would immediately start running
   * the returned TaskInstance.
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillSucceed: computed('nextPerformState', function() {
    return isSuccess(this.get('nextPerformState'));
  }),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would immediately cancel (drop)
   * the returned TaskInstance.
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillDrop: computed.equal('nextPerformState', 'drop'),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would enqueue the TaskInstance
   * rather than execute immediately.
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillEnqueue: computed.equal('nextPerformState', 'enqueue'),

  /**
   * EXPERIMENTAL
   *
   * Returns true if calling .perform() right now would cause a previous task to be canceled
   *
   * @memberof Task
   * @instance
   * @private
   * @readOnly
   */
  performWillCancelPrevious: computed.equal('nextPerformState', 'cancel_previous'),

  init() {
    this._super(...arguments);
    this._activeTaskInstances = Ember.A();
    this._queuedTaskInstances = Ember.A();

    let self = this;
    this.perform = function(...args) {
      if (this !== self) {
        console.warn(`The use of ${self._propertyName}.perform within a template is deprecated and won't be supported in future versions of ember-concurrency. Please use the \`perform\` helper instead, e.g. {{perform ${self._propertyName}}}`);
      }
      return self._perform(...args);
    };

    this._needsFlush = Ember.run.bind(this, this._scheduleFlush);

    _cleanupOnDestroy(this.context, this, 'cancelAll');
  },

  /**
   * The current number of active running task instances. This
   * number will never exceed maxConcurrency.
   *
   * @memberof Task
   * @instance
   * @readOnly
   */
  concurrency: 0,

  /**
   * Creates a new {@linkcode TaskInstance} and attempts to run it right away.
   * If running this task instance would increase the task's concurrency
   * to a number greater than the task's maxConcurrency, this task
   * instance might be immediately canceled (dropped), or enqueued
   * to run at later time, after the currently running task(s) have finished.
   *
   * @method perform
   * @memberof Task
   * @param {*} arg* - args to pass to the task function
   * @instance
   */
  perform: null,

  /**
   * Cancels all running or queued `TaskInstance`s for this Task.
   * If you're trying to cancel a specific TaskInstance (rather
   * than all of the instances running under this task) call
   * `.cancel()` on the specific TaskInstance.
   *
   * @method cancelAll
   * @memberof Task
   * @instance
   */
  cancelAll() {
    this.spliceTaskInstances(this._activeTaskInstances, 0, this._activeTaskInstances.length);
    this.spliceTaskInstances(this._queuedTaskInstances, 0, this._queuedTaskInstances.length);
  },

  spliceTaskInstances(taskInstances, index, count) {
    for (let i = index; i < index + count; ++i) {
      taskInstances[i].cancel();
    }
    taskInstances.splice(index, count);
  },

  toString() {
    return `<Task:${Ember.guidFor(this)}> of ${this._origin}`;
  },

  _perform(...args) {
    let taskInstance = TaskInstance.create({
      fn: this.fn,
      args,
      context: this.context,
      task: this,
      _origin: this,
    });

    if (this.get('_performs') && !this.get('performWillSucceed')) {
      // tasks linked via .performs() should be immediately dropped
      // before they even have a chance to run if we know that
      // .performWillSucceed is false.
      taskInstance.cancel();
      return taskInstance;
    }

    this._queuedTaskInstances.push(taskInstance);
    this._needsFlush();
    this.notifyPropertyChange('nextPerformState');

    return taskInstance;
  },

  _flushScheduled: false,
  _scheduleFlush() {
    this._flushScheduled = true;
    Ember.run.once(this, this._flushQueues);
  },

  _flushQueues() {
    this._flushScheduled = false;
    this._activeTaskInstances = Ember.A(this._activeTaskInstances.filterBy('isFinished', false));

    this.bufferPolicy.schedule(this);

    for (let i = 0; i < this._activeTaskInstances.length; ++i) {
      let taskInstance = this._activeTaskInstances[i];
      if (!taskInstance.hasStarted) {
        // use internal promise so that it doesn't cancel error reporting
        taskInstance._start()._defer.promise.then(this._needsFlush, this._needsFlush);
      }
    }

    this.notifyPropertyChange('nextPerformState');

    let concurrency = this._activeTaskInstances.length;
    this.set('concurrency', concurrency);
    if (this._completionDefer && concurrency === 0) {
      this._completionDefer.resolve();
      this._completionDefer = null;
    }
  },

  _completionDefer: null,
  _getCompletionPromise() {
    return new Ember.RSVP.Promise(r => {
      Ember.run.schedule('actions', null, () => {
        let defer = Ember.RSVP.defer();
        if (!this._flushScheduled &&
            this._activeTaskInstances.length === 0 &&
            this._queuedTaskInstances.length === 0) {
          defer.resolve();
        } else {
          this._completionDefer = defer;
        }
        defer.promise.then(r);
      });
    });
  },
});

/**
  A {@link TaskProperty} is the Computed Property-like object returned
  from the {@linkcode task} function. You can call Task Modifier methods
  on this object to configure the behavior of the {@link Task}.

  See [Managing Task Concurrency](/#/docs/task-concurrency) for an
  overview of all the different task modifiers you can use and how
  they impact automatic cancelation / enqueueing of task instances.

  <style>
    .ignore-this--this-is-here-to-hide-constructor,
    #TaskProperty { display: none }
  </style>

  @class TaskProperty
*/
export function TaskProperty(taskFn) {
  let tp = this;
  ComputedProperty.call(this, function(_propertyName) {
    return Task.create({
      fn: taskFn,
      context: this,
      _origin: this,
      bufferPolicy: tp.bufferPolicy,
      _maxConcurrency: tp._maxConcurrency,
      _performsPath: tp._performsPath && tp._performsPath[0],
      _propertyName,
      _debugName: "",
    });
  });

  this.bufferPolicy = enqueueTasksPolicy;
  this._maxConcurrency = Infinity;
  this.eventNames = null;
  this.cancelEventNames = null;
  this._performsPath = null;
}

TaskProperty.prototype = Object.create(ComputedProperty.prototype);
TaskProperty.prototype.constructor = TaskProperty;
TaskProperty.prototype.setup = function(proto, keyname) {
  addListenersToPrototype(proto, this.eventNames, keyname, '_perform');
  addListenersToPrototype(proto, this.cancelEventNames, keyname, 'cancelAll');
};

/**
 * Calling `task(...).on(eventName)` configures the task to be
 * automatically performed when the specified events fire. In
 * this way, it behaves like
 * [Ember.on](http://emberjs.com/api/classes/Ember.html#method_on).
 *
 * You can use `task(...).on('init')` to perform the task
 * when the host object is initialized.
 *
 * ```js
 * export default Ember.Component.extend({
 *   pollForUpdates: task(function * () {
 *     // ... this runs when the Component is first created
 *     // because we specified .on('init')
 *   }).on('init'),
 *
 *   handleFoo: task(function * (a, b, c) {
 *     // this gets performed automatically if the 'foo'
 *     // event fires on this Component,
 *     // e.g., if someone called component.trigger('foo')
 *   }).on('foo'),
 * });
 * ```
 *
 * [See the Writing Tasks Docs for more info](/#/docs/writing-tasks)
 *
 * @method on
 * @memberof TaskProperty
 * @param {String} eventNames*
 * @instance
 */
TaskProperty.prototype.on = function() {
  this.eventNames = this.eventNames || [];
  this.eventNames.push.apply(this.eventNames, arguments);
  return this;
};

/**
 * This behaves like the {@linkcode TaskProperty#on task(...).on() modifier},
 * but instead will cause the task to be canceled if any of the
 * specified events fire on the parent object.
 *
 * [See the Live Example](/#/docs/examples/route-tasks/1)
 *
 * @method cancelOn
 * @memberof TaskProperty
 * @param {String} eventNames*
 * @instance
 */
TaskProperty.prototype.cancelOn = function() {
  this.cancelEventNames = this.cancelEventNames || [];
  this.cancelEventNames.push.apply(this.cancelEventNames, arguments);
  return this;
};

/**
 * Configures the task to cancel old currently task instances
 * to make room for a new one to perform. Sets default
 * maxConcurrency to 1.
 *
 * [See the Live Example](/#/docs/examples/route-tasks/1)
 *
 * @method restartable
 * @memberof TaskProperty
 * @instance
 */
TaskProperty.prototype.restartable = function() {
  this.bufferPolicy = cancelOngoingTasksPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * Configures the task to run task instances one-at-a-time in
 * the order they were `.perform()`ed. Sets default
 * maxConcurrency to 1.
 *
 * @method enqueue
 * @memberof TaskProperty
 * @instance
 */
TaskProperty.prototype.enqueue = function() {
  this.bufferPolicy = enqueueTasksPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * Configures the task to immediately cancel (i.e. drop) any
 * task instances performed when the task is already running
 * at maxConcurrency. Sets default maxConcurrency to 1.
 *
 * @method drop
 * @memberof TaskProperty
 * @instance
 */
TaskProperty.prototype.drop = function() {
  this.bufferPolicy = dropQueuedTasksPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * @private
 */
TaskProperty.prototype.keepLatest = function() {
  this.bufferPolicy = dropButKeepLatestPolicy;
  this._setDefaultMaxConcurrency(1);
  return this;
};

/**
 * Sets the maximum number of task instances that are allowed
 * to run at the same time. By default, with no task modifiers
 * applied, this number is Infinity (there is no limit
 * to the number of tasks that can run at the same time).
 * {@linkcode TaskProperty#restartable .restartable()},
 * {@linkcode TaskProperty#enqueue .enqueue()}, and
 * {@linkcode TaskProperty#drop .drop()} set the default
 * maxConcurrency to 1, but you can override this value
 * to set the maximum number of concurrently running tasks
 * to a number greater than 1.
 *
 * [See the AJAX Throttling example](/#/docs/examples/ajax-throttling)
 *
 * The example below uses a task with `maxConcurrency(3)` to limit
 * the number of concurrent AJAX requests (for anyone using this task)
 * to 3.
 *
 * ```js
 * doSomeAjax: task(function * (url) {
 *   return Ember.$.getJSON(url).promise();
 * }).maxConcurrency(3),
 *
 * elsewhere() {
 *   this.get('doSomeAjax').perform("http://www.example.com/json");
 * },
 * ```
 *
 * @method maxConcurrency
 * @memberof TaskProperty
 * @param {Number} n The maximum number of concurrently running tasks
 * @instance
 */
TaskProperty.prototype.maxConcurrency = function(n) {
  this._maxConcurrency = n;
  return this;
};

TaskProperty.prototype._setDefaultMaxConcurrency = function(n) {
  if (this._maxConcurrency === Infinity) {
    this._maxConcurrency = n;
  }
};

TaskProperty.prototype.performs = function() {
  this._performsPath = this._performsPath || [];
  this._performsPath.push.apply(this._performsPath, arguments);
  return this;
};

function addListenersToPrototype(proto, eventNames, taskName, taskMethod) {
  if (eventNames) {
    for (let i = 0; i < eventNames.length; ++i) {
      let eventName = eventNames[i];
      Ember.addListener(proto, eventName, null, makeListener(taskName, taskMethod));
    }
  }
}

function makeListener(taskName, method) {
  return function() {
    let task = this.get(taskName);
    task[method].apply(task, arguments);
  };
}

<|endoftext|>


<commit_before>import Ember from 'ember';

const { isEmpty, get, computed, String: { htmlSafe } } = Ember;
const { dasherize } = Ember.String;
const { keys } = Object;

/**
 * Lifted from Ember Metal Computed Macros
 */

function getProperties(self, propertyNames) {
  var ret = {};
  for (var i = 0; i < propertyNames.length; i++) {
    ret[propertyNames[i]] = get(self, propertyNames[i]);
  }
  return ret;
}

function generateComputedWithProperties(macro) {
  return function(...properties) {
    var computedFunc = computed(function() {
      return macro.apply(this, [getProperties(this, properties)]);
    });

    return computedFunc.property.apply(computedFunc, properties);
  };
}

// Lifted from React
const isUnitlessNumber = {
  animationIterationCount: true,
  boxFlex: true,
  boxFlexGroup: true,
  boxOrdinalGroup: true,
  columnCount: true,
  flex: true,
  flexGrow: true,
  flexPositive: true,
  flexShrink: true,
  flexNegative: true,
  flexOrder: true,
  gridRow: true,
  gridColumn: true,
  fontWeight: true,
  lineClamp: true,
  lineHeight: true,
  opacity: true,
  order: true,
  orphans: true,
  tabSize: true,
  widows: true,
  zIndex: true,
  zoom: true,

  // SVG-related properties
  fillOpacity: true,
  stopOpacity: true,
  strokeDashoffset: true,
  strokeOpacity: true,
  strokeWidth: true,
};

/**
 * @param {string} prefix vendor-specific prefix, eg: Webkit
 * @param {string} key style name, eg: transitionDuration
 * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
 * WebkitTransitionDuration
 */
function prefixKey(prefix, key) {
  return prefix + key.charAt(0).toUpperCase() + key.substring(1);
}

/**
 * Support style names that may come passed in prefixed by adding permutations
 * of vendor prefixes.
 */
var prefixes = ['Webkit', 'ms', 'Moz', 'O'];

// Using keys here, or else the vanilla for-in loop makes IE8 go into an
// infinite loop, because it iterates over the newly added props too.
keys(isUnitlessNumber).forEach((prop) => {
  prefixes.forEach((prefix) => {
    isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
  });
});

function transformStyleValue(name, value) {
  if (isEmpty(value)) { return ''; }

  let isNonNumeric = isNaN(value);
  if (isNonNumeric || value === 0 ||
      isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) {
    return '' + value; // cast to string
  }

  if (typeof value === 'string') {
    value = value.trim();
  }
  return value + 'px';
}

function objectToStyleString(object) {
  return keys(object).map((name) => {
    let value = transformStyleValue(name, object[name]);
    if (isEmpty(value)) { return null; }
    return `${ dasherize(name) }:${ value }`;
  }).filter((rule) => rule !== null).join(';');
}

/**
 * Computes a style string from the value of bound properties.
 */
export const computedStyle = generateComputedWithProperties(function computedStyleProperties(properties) {
  let styleStrings = [];

  keys(properties).forEach((dependentKey) => {
    styleStrings.push(objectToStyleString(properties[dependentKey]));
  });

  let styleString = styleStrings.join(';');

  if (styleString.length > 1 && styleString.charAt(styleString.length-1) !== ';') {
    styleString+=';';
  }

  return htmlSafe(styleString);
});
<commit_msg>adjust code style with ES6<commit_after>import Ember from 'ember';

const { isEmpty, get, computed, String: { htmlSafe } } = Ember;
const { dasherize } = Ember.String;
const { keys } = Object;

/**
 * Lifted from Ember Metal Computed Macros
 */

function getProperties(self, propertyNames) {
  const ret = {};
  for (let i = 0; i < propertyNames.length; i++) {
    ret[propertyNames[i]] = get(self, propertyNames[i]);
  }
  return ret;
}

function generateComputedWithProperties(macro) {
  return function (...properties) {
    const computedFunc = computed(function () {
      return macro.apply(this, [getProperties(this, properties)]);
    });

    return computedFunc.property.apply(computedFunc, properties);
  };
}

// Lifted from React
const isUnitlessNumber = {
  animationIterationCount: true,
  boxFlex: true,
  boxFlexGroup: true,
  boxOrdinalGroup: true,
  columnCount: true,
  flex: true,
  flexGrow: true,
  flexPositive: true,
  flexShrink: true,
  flexNegative: true,
  flexOrder: true,
  gridRow: true,
  gridColumn: true,
  fontWeight: true,
  lineClamp: true,
  lineHeight: true,
  opacity: true,
  order: true,
  orphans: true,
  tabSize: true,
  widows: true,
  zIndex: true,
  zoom: true,

  // SVG-related properties
  fillOpacity: true,
  stopOpacity: true,
  strokeDashoffset: true,
  strokeOpacity: true,
  strokeWidth: true,
};

/**
 * @param {string} prefix vendor-specific prefix, eg: Webkit
 * @param {string} key style name, eg: transitionDuration
 * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
 * WebkitTransitionDuration
 */
function prefixKey(prefix, key) {
  return prefix + key.charAt(0).toUpperCase() + key.substring(1);
}

/**
 * Support style names that may come passed in prefixed by adding permutations
 * of vendor prefixes.
 */
const prefixes = ['Webkit', 'ms', 'Moz', 'O'];

// Using keys here, or else the vanilla for-in loop makes IE8 go into an
// infinite loop, because it iterates over the newly added props too.
keys(isUnitlessNumber).forEach((prop) => {
  prefixes.forEach((prefix) => {
    isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
  });
});

function transformStyleValue(name, value) {
  if (isEmpty(value)) { return ''; }

  const isNonNumeric = isNaN(value);
  let valueTransformed = '';
  if (isNonNumeric || value === 0 ||
      isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) {
    valueTransformed = `${value}`; // cast to string
  }

  if (typeof value === 'string') {
    valueTransformed = value.trim();
  }
  return `${valueTransformed}px`;
}

function objectToStyleString(object) {
  return keys(object).map((name) => {
    const value = transformStyleValue(name, object[name]);
    if (isEmpty(value)) { return null; }
    return `${dasherize(name)}:${value}`;
  })
  .filter((rule) => rule !== null)
  .join(';');
}

/**
 * Computes a style string from the value of bound properties.
 */
export const computedStyle = generateComputedWithProperties(
  function computedStyleProperties(properties) {
    const styleStrings = [];

    keys(properties).forEach((dependentKey) => {
      styleStrings.push(objectToStyleString(properties[dependentKey]));
    });

    let styleString = styleStrings.join(';');

    if (styleString.length > 1 && styleString.charAt(styleString.length - 1) !== ';') {
      styleString += ';';
    }

    return htmlSafe(styleString);
  }
);
<|endoftext|>


<commit_before>/**
 * Provides Form and input features.
 *
 * @module furnace
 * @submodule furnace-forms
 */
import Control from './abstract';
import Ember from 'ember';

import getName from 'furnace-forms/utils/get-name';
import ControlSupport from 'furnace-forms/mixins/controls/control-support';
import Proxy from 'furnace-forms/proxy';
/**
 * Panel control component proxy
 *
 * @class Panel
 * @namespace Furnace.Forms.Controls
 * @extends Furnace.Forms.Controls.Abstract
 * @protected
 */
export default Control.extend(ControlSupport,{
	'for' : null,

	_decoratorName : 'panel',

	_decoratorType: 'panel',

	_model : Ember.computed.alias('for'),

	modelName: Ember.computed.alias('_modelName'),

	init: function() {
		this._super();
	},

	actions: {
		apply:function() {
			this._apply();
		},
		reset : function() {
			this._reset();
		},
	},

	setEnabled: function(enabled) {
		if(enabled!==this._enabled) {
			this.set('_enabled',enabled);
		}
		this.setFlag('isEnabled',this._enabled && (!this._panel || (this.get('_panel.isEnabled') && this.get('_panel.hasPrerequisites')!==false)));
	},

	_panelPrerequisitesObserver:Ember.observer('_panel._condition',function() {
		this.setEnabled(this._enabled);
	}),

	_modelName : Ember.computed('_model',{
		get : function(key,value) {
			if(value)  {
				return value;
			}
			if(this.get('_model')===this.get('_panel._model')) {
				return this.get('_panel._modelName');
			}
			else if(this.get('_model')) {
				var name= getName(this.get('_model'),true);
				if(name) {
					return name;
				}
				// FIXME: our parent panel may not be an instance of panel or form (list for example)
				// causing the modelName to be undefined
				return this.get('_panel._modelName')+'.'+this._name;
			}
			else {
				return null;
			}
		},
		set : function(key,value) {
			return value;
		}
	}),

	_targetName : Ember.computed('target',{
		get : function() {
			if(this.get('target')) {
				return getName(this.get('target'));
			}
			else {
				return null;
			}
		}
	}).readOnly(),

	caption : null,

	_apply:function() {
		this._super();
		// TODO: If input controls haven't been accessed yet (we don't have a model for example) they initialize now (on submit for example) not sure whether this is desired

		this.get('inputControls').invoke('_apply');
		// Our proxy should perform all applies, so make sure we only invoke it once instead once for every child of top form proxy .
		// We may need to check our proxies top model matched parent panel proxies top model
		//if((!this._form || this.get('_model')!==this._form.get('_model')) && this.get('_model') instanceof Proxy) {
		if((!this._form ) && this.get('_model') instanceof Proxy) {
			this.get('_model')._apply();
		}
	},

	_registerControl : function(control) {
		if(this._path !== this._panel._path) {
			this._form._registerControl(control);
		}
	},

	_unregisterControl : function(control) {
		if(this._path !== this._panel._path) {
			this._form._registerControl(control);
		}
	},

	_reset:function(modelChanged) {
		this._super(modelChanged);
	},

}).reopenClass({

	generate : function(mixins,meta,options) {
		options=options || {};
		var _options={};
		mixins=mixins || [];
		if(!options['for'] && !meta.options['for'] && !this._modelSet) {
//			if(options._panel && options._panel._isFormOption) {
//				options['for']=Ember.computed.alias('_panel.for');
//			} else
//			if(options._name!==null && Ember.Enumerable.detect(options._panel.get('_model'))) {
////				if(options._panel.get('_model') instanceof DS.ManyArray) {
//					_options['for']=Ember.computed('_panel._model.[]',function() {
//						if(!this.get('_panel._model')) {
//							return undefined;
//						}
//						return this.get('_panel._model').objectAt(this._name);
//					});
////				} else {
////					_options['for']=Ember.computed.alias('_panel._model.'+options._name);
////				}
//			}
//			else
			if(options._panel.get('_model') && options._panel.get('_model.'+options._name)!==undefined) {
				_options['for']=Ember.computed.alias('_panel._model.'+options._name);
			} else {
				_options['for']=Ember.computed.alias('_panel._model');
			}
			delete options['for'];
		}
		mixins.push(meta.options);
		mixins.push(_options);
		var typeKey=this.typeKey;
		var control = this.extend.apply(this,mixins);
		control.typeKey=typeKey;
		return control;
	}
});<commit_msg>Only apply controls when a panel has a model or controls were loaded<commit_after>/**
 * Provides Form and input features.
 *
 * @module furnace
 * @submodule furnace-forms
 */
import Control from './abstract';
import Ember from 'ember';

import getName from 'furnace-forms/utils/get-name';
import ControlSupport from 'furnace-forms/mixins/controls/control-support';
import Proxy from 'furnace-forms/proxy';
/**
 * Panel control component proxy
 *
 * @class Panel
 * @namespace Furnace.Forms.Controls
 * @extends Furnace.Forms.Controls.Abstract
 * @protected
 */
export default Control.extend(ControlSupport,{
	'for' : null,

	_decoratorName : 'panel',

	_decoratorType: 'panel',

	_model : Ember.computed.alias('for'),

	modelName: Ember.computed.alias('_modelName'),

	init: function() {
		this._super();
	},

	actions: {
		apply:function() {
			this._apply();
		},
		reset : function() {
			this._reset();
		},
	},

	setEnabled: function(enabled) {
		if(enabled!==this._enabled) {
			this.set('_enabled',enabled);
		}
		this.setFlag('isEnabled',this._enabled && (!this._panel || (this.get('_panel.isEnabled') && this.get('_panel.hasPrerequisites')!==false)));
	},

	_panelPrerequisitesObserver:Ember.observer('_panel._condition',function() {
		this.setEnabled(this._enabled);
	}),

	_modelName : Ember.computed('_model',{
		get : function(key,value) {
			if(value)  {
				return value;
			}
			if(this.get('_model')===this.get('_panel._model')) {
				return this.get('_panel._modelName');
			}
			else if(this.get('_model')) {
				var name= getName(this.get('_model'),true);
				if(name) {
					return name;
				}
				// FIXME: our parent panel may not be an instance of panel or form (list for example)
				// causing the modelName to be undefined
				return this.get('_panel._modelName')+'.'+this._name;
			}
			else {
				return null;
			}
		},
		set : function(key,value) {
			return value;
		}
	}),

	_targetName : Ember.computed('target',{
		get : function() {
			if(this.get('target')) {
				return getName(this.get('target'));
			}
			else {
				return null;
			}
		}
	}).readOnly(),

	caption : null,

	_apply:function() {
		this._super();

		// Only apply controls when we have a model or controls were loaded
		if(this.get('_model') || this._controls!==null) {
			this.get('inputControls').invoke('_apply');
		}

		// Our proxy should perform all applies, so make sure we only invoke it once instead once for every child of top form proxy .
		// We may need to check our proxies top model matched parent panel proxies top model
		//if((!this._form || this.get('_model')!==this._form.get('_model')) && this.get('_model') instanceof Proxy) {
		if((!this._form ) && this.get('_model') instanceof Proxy) {
			this.get('_model')._apply();
		}
	},

	_registerControl : function(control) {
		if(this._path !== this._panel._path) {
			this._form._registerControl(control);
		}
	},

	_unregisterControl : function(control) {
		if(this._path !== this._panel._path) {
			this._form._registerControl(control);
		}
	},

	_reset:function(modelChanged) {
		this._super(modelChanged);
	},

}).reopenClass({

	generate : function(mixins,meta,options) {
		options=options || {};
		var _options={};
		mixins=mixins || [];
		if(!options['for'] && !meta.options['for'] && !this._modelSet) {
//			if(options._panel && options._panel._isFormOption) {
//				options['for']=Ember.computed.alias('_panel.for');
//			} else
//			if(options._name!==null && Ember.Enumerable.detect(options._panel.get('_model'))) {
////				if(options._panel.get('_model') instanceof DS.ManyArray) {
//					_options['for']=Ember.computed('_panel._model.[]',function() {
//						if(!this.get('_panel._model')) {
//							return undefined;
//						}
//						return this.get('_panel._model').objectAt(this._name);
//					});
////				} else {
////					_options['for']=Ember.computed.alias('_panel._model.'+options._name);
////				}
//			}
//			else
			if(options._panel.get('_model') && options._panel.get('_model.'+options._name)!==undefined) {
				_options['for']=Ember.computed.alias('_panel._model.'+options._name);
			} else {
				_options['for']=Ember.computed.alias('_panel._model');
			}
			delete options['for'];
		}
		mixins.push(meta.options);
		mixins.push(_options);
		var typeKey=this.typeKey;
		var control = this.extend.apply(this,mixins);
		control.typeKey=typeKey;
		return control;
	}
});<|endoftext|>


<commit_before>/**
  @module ember-flexberry-data
*/

import Ember from 'ember';
import Dexie from 'npm:dexie';
import Queue from '../utils/queue';
import isEmbedded from '../utils/is-embedded';

const { isArray, get, merge } = Ember;

/**
  Service for storing [Dexie](https://github.com/dfahlander/Dexie.js) instance for application.

  @class DexieService
  @namespace Offline
  @extends Ember.Service
  @public
*/
export default Ember.Service.extend(Ember.Evented, {
  /**
    Contains instances of Dexie.

    @property _dexie
    @type Object
  */
  _dexie: {},

  /**
    Count of objects that should be synced down.

    @property queueSyncDownWorksCount
    @type Number
    @default 0
  */
  queueSyncDownWorksCount: 0,

  /**
    Count of objects that should be synced up.

    @property queueSyncUpWorksCount
    @type Number
    @default 0
  */
  queueSyncUpWorksCount: 0,

  /**
    Total count of objects that should be synced up on current sync up operation.

    @property queueSyncUpTotalWorksCount
    @type Number
    @default 0
  */
  queueSyncUpTotalWorksCount: 0,

  /**
    Total count of objects that should be synced up on current sync up operation.

    @property queueSyncUpTotalWorksCount
    @type String
    @default null
  */
  queueSyncUpCurrentModelName: null,

  /**
    Return the only instance of Dexie database with specified schemas.
    Schemas are specified in base store in `offlineSchema` property.

    @method dexie
    @param {String} dbName
    @param {DS.Store or subclass} store
    @param {Object} [options]
    @param {Array} [options.addons]
    @param {Boolean} [options.autoOpen]
    @param {IDBFactory} [options.indexedDB]
    @param {IDBKeyRange} [options.IDBKeyRange]
    @return {Dexie} Dexie database.
  */
  dexie(dbName, store, options) {
    let dexie = this.get('_dexie')[dbName];
    if (dexie instanceof Dexie) {
      return dexie;
    }

    let db =  new Dexie(dbName, merge({}, options));
    let schemas = store.get('offlineSchema')[dbName];
    for (let version in schemas) {
      db.version(version).stores(schemas[version]);
    }

    db.tables.forEach((table) => {
      let TableClass = table.defineClass({});
      let modelClass = store.modelFor(table.name);
      let relationshipNames = get(modelClass, 'relationshipNames');
      let relationshipsByName = get(modelClass, 'relationshipsByName');
      let primaryKeyNameFromSerializer = store.serializerFor(table.name).get('primaryKey');
      let primaryKeyName = primaryKeyNameFromSerializer ? primaryKeyNameFromSerializer : 'id';

      TableClass.prototype.loadRelationships = function(projection) {
        let promises = [];
        let relationshipsToIterate = Ember.A();

        if (projection && typeof projection === 'string') {
          projection = get(modelClass, 'projections').get(projection);
        }

        if (projection) {
          // Killing two birds with one stone.
          for (let attributeName in this) {
            if (this.hasOwnProperty(attributeName)) {
              if (attributeName !== primaryKeyName && !projection.attributes.hasOwnProperty(attributeName)) {
                delete this[attributeName];
              }

              if (projection.attributes.hasOwnProperty(attributeName) &&
              (projection.attributes[attributeName].kind === 'belongsTo' || projection.attributes[attributeName].kind === 'hasMany')) {
                relationshipsToIterate.pushObject(attributeName);
              }
            }
          }
        } else {
          relationshipsToIterate.pushObjects(relationshipNames.belongsTo);
          relationshipsToIterate.pushObjects(relationshipNames.hasMany);
        }

        relationshipsToIterate.forEach((name) => {
          let relationship = relationshipsByName.get(name);
          let saveRelationship = (hash) => {
            if (!hash) {
              throw new Error(`Not found relationship with key '${this[name]}' in '${relationship.type}' table.`);
            }

            if (relationship.options.inverse) {
              if (!projection || !get(projection, `attributes.${name}.attributes.${relationship.options.inverse}`)) {
                hash[relationship.options.inverse] = null;
              }
            }

            if (relationship.kind === 'hasMany') {
              for (let i = 0; i < this[name].length; i++) {
                if (this[name][i] === hash.id) {
                  this[name][i] = hash;
                }
              }
            } else if (relationship.kind === 'belongsTo') {
              this[name] = hash;
            }

            return hash.loadRelationships(projection && projection.attributes[name]);
          };

          if (this[name] && !relationship.options.async && isEmbedded(store, modelClass, name)) {
            let ids = isArray(this[name]) ? this[name] : [this[name]];
            ids.forEach((id) => {
              promises.push(db.table(relationship.type).get(id, saveRelationship));
            });
          }
        });
        return Dexie.Promise.all(promises);
      };
    });

    this.get(`_dexie`)[dbName] = db;
    return db;
  },

  /**
    Add operation to queue of Dexie oprations.

    @method performQueueOperation
    @param {Dexie} db
    @param {Function} operation
    @return {Promise} Promise for added to queue operation.
  */
  performQueueOperation(db, operation) {
    return this._queue.attach((resolve, reject) => {
      if (!db.isOpen()) {
        db.open().then((db) => {
          operation(db).then(() => {
            resolve();
          }).catch(reject);
        }).catch(reject);
      } else {
        operation(db).then(() => {
          resolve();
        }).catch(reject);
      }
    });
  },

  /**
    Perform Dexie opration without adding it to queue.

    @method performOperation
    @param {Dexie} db
    @param {Function} operation
    @return {Promise} Result of performed operation.
  */
  performOperation(db, operation) {
    if (!db.isOpen()) {
      return db.open().then((db) => operation(db));
    } else {
      return operation(db);
    }
  },

  /* Queue for requests to Dexie */
  _queue: Queue.create()
});
<commit_msg>Feature dexie service add extend for projection<commit_after>/**
  @module ember-flexberry-data
*/

import Ember from 'ember';
import Dexie from 'npm:dexie';
import Queue from '../utils/queue';
import isEmbedded from '../utils/is-embedded';

const { isArray, get, merge } = Ember;

/**
  Service for storing [Dexie](https://github.com/dfahlander/Dexie.js) instance for application.

  @class DexieService
  @namespace Offline
  @extends Ember.Service
  @public
*/
export default Ember.Service.extend(Ember.Evented, {
  /**
    Contains instances of Dexie.

    @property _dexie
    @type Object
  */
  _dexie: {},

  /**
    Count of objects that should be synced down.

    @property queueSyncDownWorksCount
    @type Number
    @default 0
  */
  queueSyncDownWorksCount: 0,

  /**
    Count of objects that should be synced up.

    @property queueSyncUpWorksCount
    @type Number
    @default 0
  */
  queueSyncUpWorksCount: 0,

  /**
    Total count of objects that should be synced up on current sync up operation.

    @property queueSyncUpTotalWorksCount
    @type Number
    @default 0
  */
  queueSyncUpTotalWorksCount: 0,

  /**
    Total count of objects that should be synced up on current sync up operation.

    @property queueSyncUpTotalWorksCount
    @type String
    @default null
  */
  queueSyncUpCurrentModelName: null,

  /**
    Return the only instance of Dexie database with specified schemas.
    Schemas are specified in base store in `offlineSchema` property.

    @method dexie
    @param {String} dbName
    @param {DS.Store or subclass} store
    @param {Object} [options]
    @param {Array} [options.addons]
    @param {Boolean} [options.autoOpen]
    @param {IDBFactory} [options.indexedDB]
    @param {IDBKeyRange} [options.IDBKeyRange]
    @return {Dexie} Dexie database.
  */
  dexie(dbName, store, options) {
    let dexie = this.get('_dexie')[dbName];
    if (dexie instanceof Dexie) {
      return dexie;
    }

    let db =  new Dexie(dbName, merge({}, options));
    let schemas = store.get('offlineSchema')[dbName];
    for (let version in schemas) {
      db.version(version).stores(schemas[version]);
    }

    db.tables.forEach((table) => {
      let TableClass = table.defineClass({});
      let modelClass = store.modelFor(table.name);
      let relationshipNames = get(modelClass, 'relationshipNames');
      let relationshipsByName = get(modelClass, 'relationshipsByName');
      let primaryKeyNameFromSerializer = store.serializerFor(table.name).get('primaryKey');
      let primaryKeyName = primaryKeyNameFromSerializer ? primaryKeyNameFromSerializer : 'id';

      TableClass.prototype.loadByProjection = function(projection, extend) {
        let promises = [];
        let relationshipsToIterate = Ember.A();

        if (projection && typeof projection === 'string') {
          projection = get(modelClass, 'projections').get(projection);
        }

        if (projection) {
          // Killing two birds with one stone.
          for (let attributeName in this) {
            if (attributeName !== primaryKeyName && this.hasOwnProperty(attributeName)) {
              if (!projection.attributes.hasOwnProperty(attributeName) && (!extend || !extend.hasOwnProperty(attributeName))) {
                delete this[attributeName];
              }

              if (projection.attributes.hasOwnProperty(attributeName) &&
              (projection.attributes[attributeName].kind === 'belongsTo' || projection.attributes[attributeName].kind === 'hasMany')) {
                relationshipsToIterate.pushObject(attributeName);
              }

              if (extend && extend.hasOwnProperty(attributeName) && relationshipsByName.get(attributeName)) {
                relationshipsToIterate.pushObject(attributeName);
              }
            }
          }
        } else {
          relationshipsToIterate.pushObjects(relationshipNames.belongsTo);
          relationshipsToIterate.pushObjects(relationshipNames.hasMany);
        }

        relationshipsToIterate.forEach((name) => {
          let relationship = relationshipsByName.get(name);
          let saveRelationship = (hash) => {
            if (!hash) {
              throw new Error(`Not found relationship with key '${this[name]}' in '${relationship.type}' table.`);
            }

            if (relationship.options.inverse) {
              if (!projection || !get(projection, `attributes.${name}.attributes.${relationship.options.inverse}`)) {
                hash[relationship.options.inverse] = null;
              }
            }

            if (relationship.kind === 'hasMany') {
              for (let i = 0; i < this[name].length; i++) {
                if (this[name][i] === hash.id) {
                  this[name][i] = hash;
                }
              }
            } else if (relationship.kind === 'belongsTo') {
              this[name] = hash;
            }

            return hash.loadByProjection(projection && projection.attributes[name], extend && extend[name]);
          };

          if (this[name] && !relationship.options.async && isEmbedded(store, modelClass, name)) {
            let ids = isArray(this[name]) ? this[name] : [this[name]];
            ids.forEach((id) => {
              promises.push(db.table(relationship.type).get(id, saveRelationship));
            });
          }
        });
        return Dexie.Promise.all(promises);
      };
    });

    this.get(`_dexie`)[dbName] = db;
    return db;
  },

  /**
    Add operation to queue of Dexie oprations.

    @method performQueueOperation
    @param {Dexie} db
    @param {Function} operation
    @return {Promise} Promise for added to queue operation.
  */
  performQueueOperation(db, operation) {
    return this._queue.attach((resolve, reject) => {
      if (!db.isOpen()) {
        db.open().then((db) => {
          operation(db).then(() => {
            resolve();
          }).catch(reject);
        }).catch(reject);
      } else {
        operation(db).then(() => {
          resolve();
        }).catch(reject);
      }
    });
  },

  /**
    Perform Dexie opration without adding it to queue.

    @method performOperation
    @param {Dexie} db
    @param {Function} operation
    @return {Promise} Result of performed operation.
  */
  performOperation(db, operation) {
    if (!db.isOpen()) {
      return db.open().then((db) => operation(db));
    } else {
      return operation(db);
    }
  },

  /* Queue for requests to Dexie */
  _queue: Queue.create()
});
<|endoftext|>


<commit_before>'use strict';

var globals = require('./globals'),
  consts = require('./consts'),
  util = require('./util'),
  vlfield = require('./field'),
  vlenc = require('./enc'),
  schema = require('./schema/schema'),
  time = require('./compile/time');

var Encoding = module.exports = (function() {

  function Encoding(marktype, enc, config, filter, theme) {
    var defaults = schema.instantiate();

    var spec = {
      marktype: marktype,
      enc: enc,
      cfg: config,
      filter: filter || []
    };

    // type to bitcode
    for (var e in defaults.enc) {
      defaults.enc[e].type = consts.dataTypes[defaults.enc[e].type];
    }

    var specExtended = schema.util.merge(defaults, theme || {}, spec) ;

    this._marktype = specExtended.marktype;
    this._enc = specExtended.enc;
    this._cfg = specExtended.cfg;
    this._filter = specExtended.filter;
  }

  var proto = Encoding.prototype;

  proto.marktype = function() {
    return this._marktype;
  };

  proto.is = function(m) {
    return this._marktype === m;
  };

  proto.has = function(encType) {
    // equivalent to calling vlenc.has(this._enc, encType)
    return this._enc[encType].name !== undefined;
  };

  proto.enc = function(x) {
    return this._enc[x];
  };

  proto.filter = function() {
    var filterNull = [],
      fields = this.fields(),
      self = this;

    util.forEach(fields, function(fieldList, fieldName) {
      if (fieldName === '*') return; //count

      if ((self.config('filterNull').Q && fieldList.containsType[Q]) ||
          (self.config('filterNull').T && fieldList.containsType[T]) ||
          (self.config('filterNull').O && fieldList.containsType[O])) {
        filterNull.push({
          operands: [fieldName],
          operator: 'notNull'
        });
      }
    });

    return filterNull.concat(this._filter);
  };

  // get "field" property for vega
  proto.field = function(x, nodata, nofn) {
    if (!this.has(x)) return null;

    var f = (nodata ? '' : 'data.');

    if (this._enc[x].aggr === 'count') {
      return f + 'count';
    } else if (!nofn && this._enc[x].bin) {
      return f + 'bin_' + this._enc[x].name;
    } else if (!nofn && this._enc[x].aggr) {
      return f + this._enc[x].aggr + '_' + this._enc[x].name;
    } else if (!nofn && this._enc[x].fn) {
      return f + this._enc[x].fn + '_' + this._enc[x].name;
    } else {
      return f + this._enc[x].name;
    }
  };

  proto.fieldName = function(x) {
    return this._enc[x].name;
  };

  /*
   * return key-value pairs of field name and list of fields of that field name
   */
  proto.fields = function() {
    return vlenc.fields(this._enc);
  };

  proto.fieldTitle = function(x) {
    if (vlfield.isCount(this._enc[x])) {
      return vlfield.count.displayName;
    }
    var fn = this._enc[x].aggr || this._enc[x].fn || (this._enc[x].bin && "bin");
    if (fn) {
      return fn.toUpperCase() + '(' + this._enc[x].name + ')';
    } else {
      return this._enc[x].name;
    }
  };

  proto.scale = function(x) {
    return this._enc[x].scale || {};
  };

  proto.axis = function(x) {
    return this._enc[x].axis || {};
  };

  proto.band = function(x) {
    return this._enc[x].band || {};
  };

  proto.bandSize = function(encType, useSmallBand) {
    useSmallBand = useSmallBand ||
      //isBandInSmallMultiples
      (encType === Y && this.has(ROW) && this.has(Y)) ||
      (encType === X && this.has(COL) && this.has(X));

    // if band.size is explicitly specified, follow the specification, otherwise draw value from config.
    return this.band(encType).size ||
      this.config(useSmallBand ? 'smallBandSize' : 'largeBandSize');
  };

  proto.aggr = function(x) {
    return this._enc[x].aggr;
  };

  // returns false if binning is disabled, otherwise an object with binning properties
  proto.bin = function(x) {
    var bin = this._enc[x].bin;
    if (bin === {})
      return false;
    if (bin === true)
      return {
        maxbins: schema.MAXBINS_DEFAULT
      };
    return bin;
  };

  proto.legend = function(x) {
    return this._enc[x].legend;
  };

  proto.value = function(x) {
    return this._enc[x].value;
  };

  proto.fn = function(x) {
    return this._enc[x].fn;
  };

   proto.sort = function(x) {
    return this._enc[x].sort;
  };

  proto.any = function(f) {
    return util.any(this._enc, f);
  };

  proto.all = function(f) {
    return util.all(this._enc, f);
  };

  proto.length = function() {
    return util.keys(this._enc).length;
  };

  proto.map = function(f) {
    return vlenc.map(this._enc, f);
  };

  proto.reduce = function(f, init) {
    return vlenc.reduce(this._enc, f, init);
  };

  proto.forEach = function(f) {
    return vlenc.forEach(this._enc, f);
  };

  proto.type = function(et) {
    return this.has(et) ? this._enc[et].type : null;
  };

  proto.role = function(et) {
    return this.has(et) ? vlfield.role(this._enc[et]) : null;
  };

  proto.text = function(prop) {
    var text = this._enc[TEXT].text;
    return prop ? text[prop] : text;
  };

  proto.font = function(prop) {
    var font = this._enc[TEXT].font;
    return prop ? font[prop] : font;
  };

  proto.isType = function(x, type) {
    var field = this.enc(x);
    return field && Encoding.isType(field, type);
  };

  Encoding.isType = function (fieldDef, type) {
    return (fieldDef.type & type) > 0;
  };

  Encoding.isOrdinalScale = function(encoding, encType) {
    return vlfield.isOrdinalScale(encoding.enc(encType), true);
  };

  Encoding.isDimension = function(encoding, encType) {
    return vlfield.isDimension(encoding.enc(encType), true);
  };

  Encoding.isMeasure = function(encoding, encType) {
    return vlfield.isMeasure(encoding.enc(encType), true);
  };

  proto.isOrdinalScale = function(encType) {
    return this.has(encType) && Encoding.isOrdinalScale(this, encType);
  };

  proto.isDimension = function(encType) {
    return this.has(encType) && Encoding.isDimension(this, encType);
  };

  proto.isMeasure = function(encType) {
    return this.has(encType) && Encoding.isMeasure(this, encType);
  };

  proto.isAggregate = function() {
    return vlenc.isAggregate(this._enc);
  };

  Encoding.isAggregate = function(spec) {
    return vlenc.isAggregate(spec.enc);
  };

  Encoding.alwaysNoOcclusion = function(spec, stats) {
    // FIXME raw OxQ with # of rows = # of O
    return vlenc.isAggregate(spec.enc);
  };

  Encoding.isStack = function(spec) {
    // FIXME update this once we have control for stack ...
    return (spec.marktype === 'bar' || spec.marktype === 'area') &&
      spec.enc.color;
  };

  proto.isStack = function() {
    // FIXME update this once we have control for stack ...
    return (this.is('bar') || this.is('area')) && this.has('color');
  };

  proto.cardinality = function(encType, stats) {
    return vlfield.cardinality(this.enc(encType), stats, this.config('filterNull'), true);
  };

  proto.isRaw = function() {
    return !this.isAggregate();
  };

  proto.config = function(name) {
    return this._cfg[name];
  };

  proto.toSpec = function(excludeConfig) {
    var enc = util.duplicate(this._enc),
      spec;

    // convert type's bitcode to type name
    for (var e in enc) {
      enc[e].type = consts.dataTypeNames[enc[e].type];
    }

    spec = {
      marktype: this._marktype,
      enc: enc,
      filter: this._filter
    };

    if (!excludeConfig) {
      spec.cfg = util.duplicate(this._cfg);
    }

    // remove defaults
    var defaults = schema.instantiate();
    return schema.util.subtract(spec, defaults);
  };

  proto.toShorthand = function() {
    var c = consts.shorthand;
    return 'mark' + c.assign + this._marktype +
      c.delim + vlenc.shorthand(this._enc);
  };

  Encoding.shorthand = function (spec) {
    var c = consts.shorthand;
    return 'mark' + c.assign + spec.marktype +
      c.delim + vlenc.shorthand(spec.enc);
  };

  Encoding.parseShorthand = function(shorthand, cfg) {
    var c = consts.shorthand,
        split = shorthand.split(c.delim),
        marktype = split.shift().split(c.assign)[1].trim(),
        enc = vlenc.parseShorthand(split, true);

    return new Encoding(marktype, enc, cfg);
  };

  // FIXME remove this -- simply use Encoding.shorthand
  Encoding.shorthandFromSpec = function(/*spec, theme*/) {
    return Encoding.fromSpec.apply(null, arguments).toShorthand();
  };

  Encoding.specFromShorthand = function(shorthand, cfg, excludeConfig) {
    return Encoding.parseShorthand(shorthand, cfg).toSpec(excludeConfig);
  };

  Encoding.fromSpec = function(spec, theme) {
    var enc = util.duplicate(spec.enc || {});

    //convert type from string to bitcode (e.g, O=1)
    for (var e in enc) {
      enc[e].type = consts.dataTypes[enc[e].type];
    }

    return new Encoding(spec.marktype, enc, spec.cfg, spec.filter, theme);
  };

  Encoding.transpose = function(spec) {
    var oldenc = spec.enc,
      enc = util.duplicate(spec.enc);
    enc.x = oldenc.y;
    enc.y = oldenc.x;
    enc.row = oldenc.col;
    enc.col = oldenc.row;
    spec.enc = enc;
    return spec;
  };

  Encoding.toggleSort = function(spec) {
    if (!Encoding.toggleSort.support(spec)) { return; }
    var enc = spec.enc;

    var oField = enc.x.type === 'O' ? enc.x : enc.y,
      qField = enc.x.type === 'O' ? enc.y : enc.x;

    // FIXME add ascending / descending
    if (oField.sort && oField.sort.length > 0) {
      oField.sort = [];
    } else {
      oField.sort = [{
        name: qField.name,
        aggr: qField.aggr,
        type: qField.type,
        reverse: true
      }];
    }

    spec.enc = enc;
    return spec;
  };

  Encoding.toggleSort.direction = function(spec) {
    if (!Encoding.toggleSort.support(spec)) { return; }
    var enc = spec.enc;
    return enc.x.type === 'O' ? 'x' :  'y';
  };

  Encoding.toggleSort.mode = function(spec) {
    if (!Encoding.toggleSort.support(spec)) { return; }
    var enc = spec.enc;

    var oField = enc.x.type === 'O' ? enc.x : enc.y,
      qField = enc.x.type === 'O' ? enc.y : enc.x;

    // FIXME add ascending / descending
    if (oField.sort && oField.sort.length > 0) {
      return 'Q';
    } else {
      return 'O';
    }
  };

  Encoding.toggleSort.support = function(spec, stats) {
    var enc = spec.enc;
    if (vlenc.has(enc, ROW) || vlenc.has(enc, COL) ||
      !vlenc.has(enc, X) || !vlenc.has(enc, Y) ||
      !Encoding.alwaysNoOcclusion(spec, stats)) {
      return false;
    }
    return (enc.x.type === 'O' && vlfield.isMeasure(enc.y)) ? 'x' :
      (enc.y.type === 'O' && vlfield.isMeasure(enc.x)) ? 'y' : false;
  };

  Encoding.toggleFilterNullO = function(spec) {
    spec.cfg.filterNull.O = !spec.cfg.filterNull.O;
    return spec;
  };

  Encoding.toggleFilterNullO.support = function(spec, stats) {
    var fields = vlenc.fields(spec.enc);
    console.log('fields', fields);
    for (var fieldName in fields) {
      var fieldList = fields[fieldName];
      if (fieldList.containsType.O && stats[fieldName].numNulls > 0) {
        return true;
      }
    }
    return false;
  };

  return Encoding;
})();
<commit_msg>Fix toggleFilterNullO so that it can handle *<commit_after>'use strict';

var globals = require('./globals'),
  consts = require('./consts'),
  util = require('./util'),
  vlfield = require('./field'),
  vlenc = require('./enc'),
  schema = require('./schema/schema'),
  time = require('./compile/time');

var Encoding = module.exports = (function() {

  function Encoding(marktype, enc, config, filter, theme) {
    var defaults = schema.instantiate();

    var spec = {
      marktype: marktype,
      enc: enc,
      cfg: config,
      filter: filter || []
    };

    // type to bitcode
    for (var e in defaults.enc) {
      defaults.enc[e].type = consts.dataTypes[defaults.enc[e].type];
    }

    var specExtended = schema.util.merge(defaults, theme || {}, spec) ;

    this._marktype = specExtended.marktype;
    this._enc = specExtended.enc;
    this._cfg = specExtended.cfg;
    this._filter = specExtended.filter;
  }

  var proto = Encoding.prototype;

  proto.marktype = function() {
    return this._marktype;
  };

  proto.is = function(m) {
    return this._marktype === m;
  };

  proto.has = function(encType) {
    // equivalent to calling vlenc.has(this._enc, encType)
    return this._enc[encType].name !== undefined;
  };

  proto.enc = function(x) {
    return this._enc[x];
  };

  proto.filter = function() {
    var filterNull = [],
      fields = this.fields(),
      self = this;

    util.forEach(fields, function(fieldList, fieldName) {
      if (fieldName === '*') return; //count

      if ((self.config('filterNull').Q && fieldList.containsType[Q]) ||
          (self.config('filterNull').T && fieldList.containsType[T]) ||
          (self.config('filterNull').O && fieldList.containsType[O])) {
        filterNull.push({
          operands: [fieldName],
          operator: 'notNull'
        });
      }
    });

    return filterNull.concat(this._filter);
  };

  // get "field" property for vega
  proto.field = function(x, nodata, nofn) {
    if (!this.has(x)) return null;

    var f = (nodata ? '' : 'data.');

    if (this._enc[x].aggr === 'count') {
      return f + 'count';
    } else if (!nofn && this._enc[x].bin) {
      return f + 'bin_' + this._enc[x].name;
    } else if (!nofn && this._enc[x].aggr) {
      return f + this._enc[x].aggr + '_' + this._enc[x].name;
    } else if (!nofn && this._enc[x].fn) {
      return f + this._enc[x].fn + '_' + this._enc[x].name;
    } else {
      return f + this._enc[x].name;
    }
  };

  proto.fieldName = function(x) {
    return this._enc[x].name;
  };

  /*
   * return key-value pairs of field name and list of fields of that field name
   */
  proto.fields = function() {
    return vlenc.fields(this._enc);
  };

  proto.fieldTitle = function(x) {
    if (vlfield.isCount(this._enc[x])) {
      return vlfield.count.displayName;
    }
    var fn = this._enc[x].aggr || this._enc[x].fn || (this._enc[x].bin && "bin");
    if (fn) {
      return fn.toUpperCase() + '(' + this._enc[x].name + ')';
    } else {
      return this._enc[x].name;
    }
  };

  proto.scale = function(x) {
    return this._enc[x].scale || {};
  };

  proto.axis = function(x) {
    return this._enc[x].axis || {};
  };

  proto.band = function(x) {
    return this._enc[x].band || {};
  };

  proto.bandSize = function(encType, useSmallBand) {
    useSmallBand = useSmallBand ||
      //isBandInSmallMultiples
      (encType === Y && this.has(ROW) && this.has(Y)) ||
      (encType === X && this.has(COL) && this.has(X));

    // if band.size is explicitly specified, follow the specification, otherwise draw value from config.
    return this.band(encType).size ||
      this.config(useSmallBand ? 'smallBandSize' : 'largeBandSize');
  };

  proto.aggr = function(x) {
    return this._enc[x].aggr;
  };

  // returns false if binning is disabled, otherwise an object with binning properties
  proto.bin = function(x) {
    var bin = this._enc[x].bin;
    if (bin === {})
      return false;
    if (bin === true)
      return {
        maxbins: schema.MAXBINS_DEFAULT
      };
    return bin;
  };

  proto.legend = function(x) {
    return this._enc[x].legend;
  };

  proto.value = function(x) {
    return this._enc[x].value;
  };

  proto.fn = function(x) {
    return this._enc[x].fn;
  };

   proto.sort = function(x) {
    return this._enc[x].sort;
  };

  proto.any = function(f) {
    return util.any(this._enc, f);
  };

  proto.all = function(f) {
    return util.all(this._enc, f);
  };

  proto.length = function() {
    return util.keys(this._enc).length;
  };

  proto.map = function(f) {
    return vlenc.map(this._enc, f);
  };

  proto.reduce = function(f, init) {
    return vlenc.reduce(this._enc, f, init);
  };

  proto.forEach = function(f) {
    return vlenc.forEach(this._enc, f);
  };

  proto.type = function(et) {
    return this.has(et) ? this._enc[et].type : null;
  };

  proto.role = function(et) {
    return this.has(et) ? vlfield.role(this._enc[et]) : null;
  };

  proto.text = function(prop) {
    var text = this._enc[TEXT].text;
    return prop ? text[prop] : text;
  };

  proto.font = function(prop) {
    var font = this._enc[TEXT].font;
    return prop ? font[prop] : font;
  };

  proto.isType = function(x, type) {
    var field = this.enc(x);
    return field && Encoding.isType(field, type);
  };

  Encoding.isType = function (fieldDef, type) {
    return (fieldDef.type & type) > 0;
  };

  Encoding.isOrdinalScale = function(encoding, encType) {
    return vlfield.isOrdinalScale(encoding.enc(encType), true);
  };

  Encoding.isDimension = function(encoding, encType) {
    return vlfield.isDimension(encoding.enc(encType), true);
  };

  Encoding.isMeasure = function(encoding, encType) {
    return vlfield.isMeasure(encoding.enc(encType), true);
  };

  proto.isOrdinalScale = function(encType) {
    return this.has(encType) && Encoding.isOrdinalScale(this, encType);
  };

  proto.isDimension = function(encType) {
    return this.has(encType) && Encoding.isDimension(this, encType);
  };

  proto.isMeasure = function(encType) {
    return this.has(encType) && Encoding.isMeasure(this, encType);
  };

  proto.isAggregate = function() {
    return vlenc.isAggregate(this._enc);
  };

  Encoding.isAggregate = function(spec) {
    return vlenc.isAggregate(spec.enc);
  };

  Encoding.alwaysNoOcclusion = function(spec, stats) {
    // FIXME raw OxQ with # of rows = # of O
    return vlenc.isAggregate(spec.enc);
  };

  Encoding.isStack = function(spec) {
    // FIXME update this once we have control for stack ...
    return (spec.marktype === 'bar' || spec.marktype === 'area') &&
      spec.enc.color;
  };

  proto.isStack = function() {
    // FIXME update this once we have control for stack ...
    return (this.is('bar') || this.is('area')) && this.has('color');
  };

  proto.cardinality = function(encType, stats) {
    return vlfield.cardinality(this.enc(encType), stats, this.config('filterNull'), true);
  };

  proto.isRaw = function() {
    return !this.isAggregate();
  };

  proto.config = function(name) {
    return this._cfg[name];
  };

  proto.toSpec = function(excludeConfig) {
    var enc = util.duplicate(this._enc),
      spec;

    // convert type's bitcode to type name
    for (var e in enc) {
      enc[e].type = consts.dataTypeNames[enc[e].type];
    }

    spec = {
      marktype: this._marktype,
      enc: enc,
      filter: this._filter
    };

    if (!excludeConfig) {
      spec.cfg = util.duplicate(this._cfg);
    }

    // remove defaults
    var defaults = schema.instantiate();
    return schema.util.subtract(spec, defaults);
  };

  proto.toShorthand = function() {
    var c = consts.shorthand;
    return 'mark' + c.assign + this._marktype +
      c.delim + vlenc.shorthand(this._enc);
  };

  Encoding.shorthand = function (spec) {
    var c = consts.shorthand;
    return 'mark' + c.assign + spec.marktype +
      c.delim + vlenc.shorthand(spec.enc);
  };

  Encoding.parseShorthand = function(shorthand, cfg) {
    var c = consts.shorthand,
        split = shorthand.split(c.delim),
        marktype = split.shift().split(c.assign)[1].trim(),
        enc = vlenc.parseShorthand(split, true);

    return new Encoding(marktype, enc, cfg);
  };

  // FIXME remove this -- simply use Encoding.shorthand
  Encoding.shorthandFromSpec = function(/*spec, theme*/) {
    return Encoding.fromSpec.apply(null, arguments).toShorthand();
  };

  Encoding.specFromShorthand = function(shorthand, cfg, excludeConfig) {
    return Encoding.parseShorthand(shorthand, cfg).toSpec(excludeConfig);
  };

  Encoding.fromSpec = function(spec, theme) {
    var enc = util.duplicate(spec.enc || {});

    //convert type from string to bitcode (e.g, O=1)
    for (var e in enc) {
      enc[e].type = consts.dataTypes[enc[e].type];
    }

    return new Encoding(spec.marktype, enc, spec.cfg, spec.filter, theme);
  };

  Encoding.transpose = function(spec) {
    var oldenc = spec.enc,
      enc = util.duplicate(spec.enc);
    enc.x = oldenc.y;
    enc.y = oldenc.x;
    enc.row = oldenc.col;
    enc.col = oldenc.row;
    spec.enc = enc;
    return spec;
  };

  Encoding.toggleSort = function(spec) {
    if (!Encoding.toggleSort.support(spec)) { return; }
    var enc = spec.enc;

    var oField = enc.x.type === 'O' ? enc.x : enc.y,
      qField = enc.x.type === 'O' ? enc.y : enc.x;

    // FIXME add ascending / descending
    if (oField.sort && oField.sort.length > 0) {
      oField.sort = [];
    } else {
      oField.sort = [{
        name: qField.name,
        aggr: qField.aggr,
        type: qField.type,
        reverse: true
      }];
    }

    spec.enc = enc;
    return spec;
  };

  Encoding.toggleSort.direction = function(spec) {
    if (!Encoding.toggleSort.support(spec)) { return; }
    var enc = spec.enc;
    return enc.x.type === 'O' ? 'x' :  'y';
  };

  Encoding.toggleSort.mode = function(spec) {
    if (!Encoding.toggleSort.support(spec)) { return; }
    var enc = spec.enc;

    var oField = enc.x.type === 'O' ? enc.x : enc.y,
      qField = enc.x.type === 'O' ? enc.y : enc.x;

    // FIXME add ascending / descending
    if (oField.sort && oField.sort.length > 0) {
      return 'Q';
    } else {
      return 'O';
    }
  };

  Encoding.toggleSort.support = function(spec, stats) {
    var enc = spec.enc;
    if (vlenc.has(enc, ROW) || vlenc.has(enc, COL) ||
      !vlenc.has(enc, X) || !vlenc.has(enc, Y) ||
      !Encoding.alwaysNoOcclusion(spec, stats)) {
      return false;
    }
    return (enc.x.type === 'O' && vlfield.isMeasure(enc.y)) ? 'x' :
      (enc.y.type === 'O' && vlfield.isMeasure(enc.x)) ? 'y' : false;
  };

  Encoding.toggleFilterNullO = function(spec) {
    spec.cfg.filterNull.O = !spec.cfg.filterNull.O;
    return spec;
  };

  Encoding.toggleFilterNullO.support = function(spec, stats) {
    var fields = vlenc.fields(spec.enc);
    console.log('fields', fields);
    for (var fieldName in fields) {
      var fieldList = fields[fieldName];
      if (fieldList.containsType.O && fieldName in stats && stats[fieldName].numNulls > 0) {
        return true;
      }
    }
    return false;
  };

  return Encoding;
})();
<|endoftext|>