<commit_before>import Realm from 'realm';

import { complement } from 'set-manipulator';

import {
  addBatchToParent,
  createRecord,
  getTotal,
  reuseNumber as reuseSerialNumber,
  NUMBER_SEQUENCE_KEYS,
} from '../utilities';

/**
 * A transaction.
 *
 * @property  {string}                  id
 * @property  {string}                  serialNumber
 * @property  {Name}                    otherParty
 * @property  {string}                  comment
 * @property  {Date}                    entryDate
 * @property  {string}                  type
 * @property  {string}                  status
 * @property  {Date}                    confirmDate
 * @property  {User}                    enteredBy
 * @property  {string}                  theirRef           External reference code.
 * @property  {TransactionCategory}     category
 * @property  {List.<TransactionItem>}  items
 * @property  {Requisition}             linkedRequisition
 */
export class Transaction extends Realm.Object {
  /**
   * Create new transaction.
   */
  constructor() {
    super();
    this.addItemsFromMasterList = this.addItemsFromMasterList.bind(this);
  }

  /**
   * Delete unfinalised transaction and associated transaction items. Throw error if transaction is
   * finalised.
   *
   * @param  {Realm}  database
   */
  destructor(database) {
    if (this.isFinalised) {
      throw new Error('Cannot delete finalised transaction');
    }

    // Recycle invoice serial number to be reused.
    if (this.isCustomerInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.CUSTOMER_INVOICE_NUMBER, this.serialNumber);
    }
    if (this.isSupplierInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.SUPPLIER_INVOICE_NUMBER, this.serialNumber);
    }
    database.delete('TransactionItem', this.items);
  }

  /**
   * Get if transaction is finalised.
   *
   * @return  {boolean}
   */
  get isFinalised() {
    return this.status === 'finalised';
  }

  /**
   * Get if transaction is confirmed.
   *
   * @return  {boolean}
   */
  get isConfirmed() {
    return this.status === 'confirmed';
  }

  /**
   * Get if transaction is incoming (i.e. a supplier invoice).
   *
   * @return  {boolean}
   */
  get isIncoming() {
    return this.type === 'supplier_invoice' || this.type === 'customer_credit';
  }

  /**
   * Get if transaction is outgoing (i.e. a customer invoice or supplier credit).
   *
   * @return  {boolean}
   */
  get isOutgoing() {
    // TODO: replace string matching with already defined getter methods.
    return this.type === 'customer_invoice' || this.type === 'supplier_credit';
  }

  /**
   * Get if transaction is a customer invoice.
   *
   * @return  {boolean}
   */
  get isCustomerInvoice() {
    return this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is a supplier invoice.
   *
   * @return  {boolean]}
   */
  get isSupplierInvoice() {
    return this.type === 'supplier_invoice';
  }

  get isPrescription() {
    return this.mode === 'dispensary' && this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is an external supplier invoice.
   *
   * @return  {boolean}
   */
  get isExternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isExternalSupplier;
  }

  /**
   * Get if transaction is an internal supplier invoice.
   *
   * @return  {boolean}
   */
  get isInternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isInternalSupplier;
  }

  /**
   * Get if transaction is an inventory adjustment.
   *
   * @return  {boolean}
   */
  get isInventoryAdjustment() {
    return this.otherParty && this.otherParty.type === 'inventory_adjustment';
  }

  /**
   * Get name of other party to transaction.
   *
   * @return  {string}
   */
  get otherPartyName() {
    return this.otherParty ? this.otherParty.name : '';
  }

  /**
   * Set other party to this transaction.
   *
   * @param  {Name}  name
   */
  setOtherParty(name) {
    this.otherParty = name;
  }

  /**
   * Get total price of items in this transaction.
   *
   * @return  {number}
   */
  get totalPrice() {
    return getTotal(this.items, 'totalPrice');
  }

  /**
   * Get total quantity of items in this transaction.
   *
   * @return  {number}
   */
  get totalQuantity() {
    return getTotal(this.items, 'totalQuantity');
  }

  /**
   * Get total number of batches in this transaction.
   *
   * @return  {number}
   */
  get numberOfBatches() {
    return getTotal(this.items, 'numberOfBatches');
  }

  /**
   * Get total number of items in this transaction.
   *
   * @return  {number}
   */
  get numberOfItems() {
    return this.items.length;
  }

  /**
   * Get if this transaction is linked to a requisition.
   *
   * @return  {boolean}
   */
  get isLinkedToRequisition() {
    return !!this.linkedRequisition;
  }

  /**
   * Get if this transaction includes a given item.
   *
   * @param   {Item}  item
   * @return  {boolean}
   */
  hasItem(item) {
    const itemId = item.realItem.id;
    return this.items.filtered('item.id == $0', itemId).length > 0;
  }

  /**
   * Add an item to this transaction. If item already exists, do nothing.
   *
   * @param  {TransactionItem}  transactionItem
   */
  addItem(transactionItem) {
    this.items.push(transactionItem);
  }

  /**
   * Add all items from the customer's master list to this transaction. Transaction
   * must be an unfinalised customer invoice.
   *
   * @param  {Realm}            database
   * @param  {Array.<string>}   selected masterlists from multiselect
   */
  addItemsFromMasterList({ database, selected }) {
    if (!this.isCustomerInvoice) {
      throw new Error(`Cannot add master lists to ${this.type}`);
    }
    if (this.isFinalised) {
      throw new Error('Cannot add items to a finalised transaction');
    }
    if (this.otherParty) {
      // Filter through masterList ids that are on multiselect list
      this.otherParty.masterLists
        .filter(masterList => selected.indexOf(masterList.id) !== -1)
        .forEach(masterList => {
          const itemsToAdd = complement(masterList.items, this.items, item => item.itemId);
          itemsToAdd.forEach(masterListItem => {
            if (!masterListItem.item.crossReferenceItem && masterListItem.item.isVisible) {
              // Do not add cross reference items as will cause duplicates.
              createRecord(database, 'TransactionItem', this, masterListItem.item);
            }
          });
        });
    }
  }

  /**
   * Remove items with the given ids from this transaction.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  itemIds
   */
  removeItemsById(database, itemIds) {
    const itemsToDelete = [];
    for (let i = 0; i < itemIds.length; i += 1) {
      const transactionItem = this.items.find(testItem => testItem.id === itemIds[i]);
      if (transactionItem.isValid()) {
        itemsToDelete.push(transactionItem);
      }
    }
    database.delete('transactionItem', itemsToDelete);
  }

  /**
   * Remove batches with given ids from this transaction. Prune any items with no remaining
   * associated batches.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  transactionBatchIds
   */
  removeTransactionBatchesById(database, transactionBatchIds) {
    if (this.isFinalised) {
      throw new Error('Cannot modify finalised transaction');
    }
    const transactionBatches = this.getTransactionBatches(database);
    const transactionBatchesToDelete = [];
    transactionBatchIds.forEach(transactionBatchId => {
      const transactionBatch = transactionBatches.find(
        matchTransactionBatch => matchTransactionBatch.id === transactionBatchId
      );
      transactionBatchesToDelete.push(transactionBatch);
    });
    database.delete('TransactionBatch', transactionBatchesToDelete);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Remove given item from transaction.
   *
   * @param   {Realm}            database
   * @param   {TransactionItem}  TransactionItem
   */
  // eslint-disable-next-line class-methods-use-this
  removeTransactionItem(database, transactionItem) {
    database.delete('TransactionItem', transactionItem);
  }

  /**
   * Adds a batch to transaction, incorporating it into a matching item or creating a new
   * item if none already exist.
   *
   * @param  {Realm}             database
   * @param  {TransactionBatch}  transactionBatch
   */
  addBatchIfUnique(database, transactionBatch) {
    const { itemBatch } = transactionBatch;
    const { item } = itemBatch;

    addBatchToParent(transactionBatch, this, () =>
      createRecord(database, 'TransactionItem', this, item)
    );
  }

  /**
   * Delete any batches and items that are not contributing to this transaction.
   *
   * @param  {Realm}  database
   */
  pruneRedundantBatchesAndItems(database) {
    const batchesToRemove = this.getTransactionBatches(database).filtered('numberOfPacks = 0');

    database.delete('TransactionBatch', batchesToRemove);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Delete any items with no associated transaction batches.
   *
   * @param  {Realm}  database
   */
  pruneBatchlessTransactionItems(database) {
    const itemsToRemove = [];
    this.items.forEach(transactionItem => {
      if (transactionItem.batches.length === 0) {
        itemsToRemove.push(transactionItem);
      }
    });
    database.delete('TransactionItem', itemsToRemove);
  }

  /**
   * Returns all transaction batches for this transaction as
   * a realm collection (can be filtered).
   *
   * @param   {Realm}                               database
   * @return  {RealmCollection.<TransactionBatch>}
   */
  getTransactionBatches(database) {
    return database.objects('TransactionBatch').filtered('transaction.id == $0', this.id);
  }

  /**
   * Confirm this transaction, generating the associated item batches, linking them
   * to their items, and setting the status to confirmed.
   *
   * @param  {Realm}  database
   */
  confirm(database) {
    if (this.isConfirmed) {
      throw new Error('Cannot confirm as transaction is already confirmed');
    }
    if (this.isFinalised) {
      throw new Error('Cannot confirm as transaction is already finalised');
    }
    const isIncomingInvoice = this.isIncoming;

    this.getTransactionBatches(database).forEach(transactionBatch => {
      const {
        itemBatch,
        batch,
        packSize,
        numberOfPacks,
        expiryDate,
        costPrice,
        sellPrice,
      } = transactionBatch;

      // Pack to one all transactions in mobile, so multiply by |packSize| to get
      // quantity and price.
      const packedToOneQuantity = numberOfPacks * packSize;
      const packedToOneCostPrice = costPrice / packSize;
      const packedToOneSellPrice = sellPrice / packSize;

      const newNumberOfPacks = isIncomingInvoice
        ? itemBatch.numberOfPacks + packedToOneQuantity
        : itemBatch.numberOfPacks - packedToOneQuantity;
      itemBatch.packSize = 1;
      itemBatch.numberOfPacks = newNumberOfPacks;
      itemBatch.expiryDate = expiryDate;
      itemBatch.batch = this.adjustBatchName(batch);
      itemBatch.costPrice = packedToOneCostPrice;
      itemBatch.sellPrice = packedToOneSellPrice;
      if (isIncomingInvoice) itemBatch.supplier = this.otherParty;

      database.save('ItemBatch', itemBatch);
    });

    this.confirmDate = new Date();
    this.status = 'confirmed';
  }

  /**
   * Get `supplierinvoice_{invoiceNumber}` of batch. Name is empty and
   * transaction is supplier invoice.
   *
   * @param   {string}  batchName
   * @return  {string}
   */
  adjustBatchName(batchName) {
    if (this.isSupplierInvoice && (!batchName || batchName === '')) {
      return `supplierinvoice_${this.serialNumber}`;
    }
    return batchName;
  }

  /**
   * Finalise this transaction, setting the status so that this transaction is
   * locked down. If it has not already been confirmed (i.e. adjustments to inventory
   * made), confirm it first.
   *
   * @param  {Realm}  database
   */
  finalise(database) {
    if (this.isFinalised) {
      throw new Error('Cannot finalise as transaction is already finalised');
    }
    // Prune all invoices except internal supplier invoices.
    if (!this.isInternalSupplierInvoice) {
      this.pruneRedundantBatchesAndItems(database);
    }
    if (!this.isConfirmed) this.confirm(database);

    this.status = 'finalised';
    database.save('Transaction', this);
  }
}

Transaction.schema = {
  name: 'Transaction',
  primaryKey: 'id',
  properties: {
    id: 'string',
    serialNumber: { type: 'string', default: 'placeholderSerialNumber' },
    otherParty: { type: 'Name', optional: true },
    comment: { type: 'string', optional: true },
    entryDate: { type: 'date', optional: true },
    type: { type: 'string', default: 'placeholderType' },
    status: { type: 'string', default: 'new' },
    confirmDate: { type: 'date', optional: true },
    enteredBy: { type: 'User', optional: true },
    theirRef: { type: 'string', optional: true },
    category: { type: 'TransactionCategory', optional: true },
    items: { type: 'list', objectType: 'TransactionItem' },
    mode: { type: 'string', default: 'store' },
    prescriber: { type: 'Prescriber', optional: true },
    linkedRequisition: { type: 'Requisition', optional: true },
    total: { type: 'float', optional: true },
    outstanding: { type: 'float', optional: true },
    insurancePolicy: { type: 'InsurancePolicy', optional: true },
  },
};

export default Transaction;
<commit_msg>Add Option to Transaction in schema<commit_after>import Realm from 'realm';

import { complement } from 'set-manipulator';

import {
  addBatchToParent,
  createRecord,
  getTotal,
  reuseNumber as reuseSerialNumber,
  NUMBER_SEQUENCE_KEYS,
} from '../utilities';

/**
 * A transaction.
 *
 * @property  {string}                  id
 * @property  {string}                  serialNumber
 * @property  {Name}                    otherParty
 * @property  {string}                  comment
 * @property  {Date}                    entryDate
 * @property  {string}                  type
 * @property  {string}                  status
 * @property  {Date}                    confirmDate
 * @property  {User}                    enteredBy
 * @property  {string}                  theirRef           External reference code.
 * @property  {TransactionCategory}     category
 * @property  {List.<TransactionItem>}  items
 * @property  {Requisition}             linkedRequisition
 */
export class Transaction extends Realm.Object {
  /**
   * Create new transaction.
   */
  constructor() {
    super();
    this.addItemsFromMasterList = this.addItemsFromMasterList.bind(this);
  }

  /**
   * Delete unfinalised transaction and associated transaction items. Throw error if transaction is
   * finalised.
   *
   * @param  {Realm}  database
   */
  destructor(database) {
    if (this.isFinalised) {
      throw new Error('Cannot delete finalised transaction');
    }

    // Recycle invoice serial number to be reused.
    if (this.isCustomerInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.CUSTOMER_INVOICE_NUMBER, this.serialNumber);
    }
    if (this.isSupplierInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.SUPPLIER_INVOICE_NUMBER, this.serialNumber);
    }
    database.delete('TransactionItem', this.items);
  }

  /**
   * Get if transaction is finalised.
   *
   * @return  {boolean}
   */
  get isFinalised() {
    return this.status === 'finalised';
  }

  /**
   * Get if transaction is confirmed.
   *
   * @return  {boolean}
   */
  get isConfirmed() {
    return this.status === 'confirmed';
  }

  /**
   * Get if transaction is incoming (i.e. a supplier invoice).
   *
   * @return  {boolean}
   */
  get isIncoming() {
    return this.type === 'supplier_invoice' || this.type === 'customer_credit';
  }

  /**
   * Get if transaction is outgoing (i.e. a customer invoice or supplier credit).
   *
   * @return  {boolean}
   */
  get isOutgoing() {
    // TODO: replace string matching with already defined getter methods.
    return this.type === 'customer_invoice' || this.type === 'supplier_credit';
  }

  /**
   * Get if transaction is a customer invoice.
   *
   * @return  {boolean}
   */
  get isCustomerInvoice() {
    return this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is a supplier invoice.
   *
   * @return  {boolean]}
   */
  get isSupplierInvoice() {
    return this.type === 'supplier_invoice';
  }

  get isPrescription() {
    return this.mode === 'dispensary' && this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is an external supplier invoice.
   *
   * @return  {boolean}
   */
  get isExternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isExternalSupplier;
  }

  /**
   * Get if transaction is an internal supplier invoice.
   *
   * @return  {boolean}
   */
  get isInternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isInternalSupplier;
  }

  /**
   * Get if transaction is an inventory adjustment.
   *
   * @return  {boolean}
   */
  get isInventoryAdjustment() {
    return this.otherParty && this.otherParty.type === 'inventory_adjustment';
  }

  /**
   * Get name of other party to transaction.
   *
   * @return  {string}
   */
  get otherPartyName() {
    return this.otherParty ? this.otherParty.name : '';
  }

  /**
   * Set other party to this transaction.
   *
   * @param  {Name}  name
   */
  setOtherParty(name) {
    this.otherParty = name;
  }

  /**
   * Get total price of items in this transaction.
   *
   * @return  {number}
   */
  get totalPrice() {
    return getTotal(this.items, 'totalPrice');
  }

  /**
   * Get total quantity of items in this transaction.
   *
   * @return  {number}
   */
  get totalQuantity() {
    return getTotal(this.items, 'totalQuantity');
  }

  /**
   * Get total number of batches in this transaction.
   *
   * @return  {number}
   */
  get numberOfBatches() {
    return getTotal(this.items, 'numberOfBatches');
  }

  /**
   * Get total number of items in this transaction.
   *
   * @return  {number}
   */
  get numberOfItems() {
    return this.items.length;
  }

  /**
   * Get if this transaction is linked to a requisition.
   *
   * @return  {boolean}
   */
  get isLinkedToRequisition() {
    return !!this.linkedRequisition;
  }

  /**
   * Get if this transaction includes a given item.
   *
   * @param   {Item}  item
   * @return  {boolean}
   */
  hasItem(item) {
    const itemId = item.realItem.id;
    return this.items.filtered('item.id == $0', itemId).length > 0;
  }

  /**
   * Add an item to this transaction. If item already exists, do nothing.
   *
   * @param  {TransactionItem}  transactionItem
   */
  addItem(transactionItem) {
    this.items.push(transactionItem);
  }

  /**
   * Add all items from the customer's master list to this transaction. Transaction
   * must be an unfinalised customer invoice.
   *
   * @param  {Realm}            database
   * @param  {Array.<string>}   selected masterlists from multiselect
   */
  addItemsFromMasterList({ database, selected }) {
    if (!this.isCustomerInvoice) {
      throw new Error(`Cannot add master lists to ${this.type}`);
    }
    if (this.isFinalised) {
      throw new Error('Cannot add items to a finalised transaction');
    }
    if (this.otherParty) {
      // Filter through masterList ids that are on multiselect list
      this.otherParty.masterLists
        .filter(masterList => selected.indexOf(masterList.id) !== -1)
        .forEach(masterList => {
          const itemsToAdd = complement(masterList.items, this.items, item => item.itemId);
          itemsToAdd.forEach(masterListItem => {
            if (!masterListItem.item.crossReferenceItem && masterListItem.item.isVisible) {
              // Do not add cross reference items as will cause duplicates.
              createRecord(database, 'TransactionItem', this, masterListItem.item);
            }
          });
        });
    }
  }

  /**
   * Remove items with the given ids from this transaction.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  itemIds
   */
  removeItemsById(database, itemIds) {
    const itemsToDelete = [];
    for (let i = 0; i < itemIds.length; i += 1) {
      const transactionItem = this.items.find(testItem => testItem.id === itemIds[i]);
      if (transactionItem.isValid()) {
        itemsToDelete.push(transactionItem);
      }
    }
    database.delete('transactionItem', itemsToDelete);
  }

  /**
   * Remove batches with given ids from this transaction. Prune any items with no remaining
   * associated batches.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  transactionBatchIds
   */
  removeTransactionBatchesById(database, transactionBatchIds) {
    if (this.isFinalised) {
      throw new Error('Cannot modify finalised transaction');
    }
    const transactionBatches = this.getTransactionBatches(database);
    const transactionBatchesToDelete = [];
    transactionBatchIds.forEach(transactionBatchId => {
      const transactionBatch = transactionBatches.find(
        matchTransactionBatch => matchTransactionBatch.id === transactionBatchId
      );
      transactionBatchesToDelete.push(transactionBatch);
    });
    database.delete('TransactionBatch', transactionBatchesToDelete);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Remove given item from transaction.
   *
   * @param   {Realm}            database
   * @param   {TransactionItem}  TransactionItem
   */
  // eslint-disable-next-line class-methods-use-this
  removeTransactionItem(database, transactionItem) {
    database.delete('TransactionItem', transactionItem);
  }

  /**
   * Adds a batch to transaction, incorporating it into a matching item or creating a new
   * item if none already exist.
   *
   * @param  {Realm}             database
   * @param  {TransactionBatch}  transactionBatch
   */
  addBatchIfUnique(database, transactionBatch) {
    const { itemBatch } = transactionBatch;
    const { item } = itemBatch;

    addBatchToParent(transactionBatch, this, () =>
      createRecord(database, 'TransactionItem', this, item)
    );
  }

  /**
   * Delete any batches and items that are not contributing to this transaction.
   *
   * @param  {Realm}  database
   */
  pruneRedundantBatchesAndItems(database) {
    const batchesToRemove = this.getTransactionBatches(database).filtered('numberOfPacks = 0');

    database.delete('TransactionBatch', batchesToRemove);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Delete any items with no associated transaction batches.
   *
   * @param  {Realm}  database
   */
  pruneBatchlessTransactionItems(database) {
    const itemsToRemove = [];
    this.items.forEach(transactionItem => {
      if (transactionItem.batches.length === 0) {
        itemsToRemove.push(transactionItem);
      }
    });
    database.delete('TransactionItem', itemsToRemove);
  }

  /**
   * Returns all transaction batches for this transaction as
   * a realm collection (can be filtered).
   *
   * @param   {Realm}                               database
   * @return  {RealmCollection.<TransactionBatch>}
   */
  getTransactionBatches(database) {
    return database.objects('TransactionBatch').filtered('transaction.id == $0', this.id);
  }

  /**
   * Confirm this transaction, generating the associated item batches, linking them
   * to their items, and setting the status to confirmed.
   *
   * @param  {Realm}  database
   */
  confirm(database) {
    if (this.isConfirmed) {
      throw new Error('Cannot confirm as transaction is already confirmed');
    }
    if (this.isFinalised) {
      throw new Error('Cannot confirm as transaction is already finalised');
    }
    const isIncomingInvoice = this.isIncoming;

    this.getTransactionBatches(database).forEach(transactionBatch => {
      const {
        itemBatch,
        batch,
        packSize,
        numberOfPacks,
        expiryDate,
        costPrice,
        sellPrice,
      } = transactionBatch;

      // Pack to one all transactions in mobile, so multiply by |packSize| to get
      // quantity and price.
      const packedToOneQuantity = numberOfPacks * packSize;
      const packedToOneCostPrice = costPrice / packSize;
      const packedToOneSellPrice = sellPrice / packSize;

      const newNumberOfPacks = isIncomingInvoice
        ? itemBatch.numberOfPacks + packedToOneQuantity
        : itemBatch.numberOfPacks - packedToOneQuantity;
      itemBatch.packSize = 1;
      itemBatch.numberOfPacks = newNumberOfPacks;
      itemBatch.expiryDate = expiryDate;
      itemBatch.batch = this.adjustBatchName(batch);
      itemBatch.costPrice = packedToOneCostPrice;
      itemBatch.sellPrice = packedToOneSellPrice;
      if (isIncomingInvoice) itemBatch.supplier = this.otherParty;

      database.save('ItemBatch', itemBatch);
    });

    this.confirmDate = new Date();
    this.status = 'confirmed';
  }

  /**
   * Get `supplierinvoice_{invoiceNumber}` of batch. Name is empty and
   * transaction is supplier invoice.
   *
   * @param   {string}  batchName
   * @return  {string}
   */
  adjustBatchName(batchName) {
    if (this.isSupplierInvoice && (!batchName || batchName === '')) {
      return `supplierinvoice_${this.serialNumber}`;
    }
    return batchName;
  }

  /**
   * Finalise this transaction, setting the status so that this transaction is
   * locked down. If it has not already been confirmed (i.e. adjustments to inventory
   * made), confirm it first.
   *
   * @param  {Realm}  database
   */
  finalise(database) {
    if (this.isFinalised) {
      throw new Error('Cannot finalise as transaction is already finalised');
    }
    // Prune all invoices except internal supplier invoices.
    if (!this.isInternalSupplierInvoice) {
      this.pruneRedundantBatchesAndItems(database);
    }
    if (!this.isConfirmed) this.confirm(database);

    this.status = 'finalised';
    database.save('Transaction', this);
  }
}

Transaction.schema = {
  name: 'Transaction',
  primaryKey: 'id',
  properties: {
    id: 'string',
    serialNumber: { type: 'string', default: 'placeholderSerialNumber' },
    otherParty: { type: 'Name', optional: true },
    comment: { type: 'string', optional: true },
    entryDate: { type: 'date', optional: true },
    type: { type: 'string', default: 'placeholderType' },
    status: { type: 'string', default: 'new' },
    confirmDate: { type: 'date', optional: true },
    enteredBy: { type: 'User', optional: true },
    theirRef: { type: 'string', optional: true },
    category: { type: 'TransactionCategory', optional: true },
    items: { type: 'list', objectType: 'TransactionItem' },
    mode: { type: 'string', default: 'store' },
    prescriber: { type: 'Prescriber', optional: true },
    linkedRequisition: { type: 'Requisition', optional: true },
    total: { type: 'float', optional: true },
    outstanding: { type: 'float', optional: true },
    insurancePolicy: { type: 'InsurancePolicy', optional: true },
    option: { type: 'Options', optional: true },
  },
};

export default Transaction;
<|endoftext|>


<commit_before>var doc = require('./lib/doc');
var field = require('./lib/field');
var genericBookmark = require('./lib/GenericBookmark');
var genericDimension = require('./lib/GenericDimension');
var genericMeasure = require('./lib/GenericMeasure');
var genericObject = require('./lib/GenericObject');
var global = require('./lib/global');
var variable = require('./lib/variable');

var WebSocket = require('ws');
var Promise = require("es6-promise").Promise;

var qSocks = {
	Doc: doc,
	Field: field,
	GenericBookmark: genericBookmark,
	GenericDimension: genericDimension,
	GenericMeasure: genericMeasure,
	GenericObject: genericObject,
	Global: global,
	Variable: variable
};

function Connect(config) {
	var cfg = {};
	if (config) {
		cfg.mark = config.mark;
		cfg.appname = config.appname || false;
		cfg.host = config.host;
		cfg.origin = config.origin;
		cfg.isSecure = config.isSecure;
		cfg.rejectUnauthorized = config.rejectUnauthorized;
		cfg.headers = config.headers || {};
	}

	return new Promise(function(resolve, reject) {
		cfg.done = function(glob) {
			resolve(glob);
		};
		cfg.error = function(msg) {
			reject(msg);
		};
		new Connection(cfg);
	});
};

qSocks.Connect = Connect;

function Connection(config) {
	var mark = (config && config.mark) ? config.mark + ': ' : '';
	var host = (config && config.host) ? config.host : 'localhost';
	var port = host === 'localhost' ? ':4848' : '';
	var isSecure = (config && config.isSecure) ? 'wss://' : 'ws://'
	var error = config ? config.error : null;
	var done = config ? config.done : null;
	this.mark = mark;
	this.seqid = 0;
	this.pending = {};
	this.handles = {};
	var self = this;
	var suffix = config.appname ? '/sense/app/' + config.appname : '';

	this.ws = new WebSocket(isSecure + host + port + suffix, null, config);

	this.ws.onopen = function(ev) {
		if (done) {
			done.call(self, new qSocks.Global(self, -1));
		};
	};
	this.ws.onerror = function(ev) {
		if (error) {
			console.log(ev.message)
		}
		self.ws = null;
	};
	this.ws.onclose = function() {
		var unexpected = self.ws != null;
		var pending = self.pending[-99];
		delete self.pending[-99];
		if (pending) {
			pending.callback();
		} else if (unexpected) {
			if (error) {
				error();
			}
		}
		self.ws = null;
	};
	this.ws.onmessage = function(ev) {
		var text = ev.data;
		var msg = JSON.parse(text);
		console.log(msg)
		var pending = self.pending[msg.id];
		delete self.pending[msg.id];
		if (pending) {
			if (msg.result) {
				pending.resolve(msg.result);
			} else {
				pending.reject(msg.error);
			}
		}
	};
}
Connection.prototype.ask = function(handle, method, args) {
	var connection = this;
	if (!Array.isArray(args)) {
		var array = [];
		for (var ix in args) {
			array[ix] = args[ix];
		}
		args = array;
	}
	var seqid = ++connection.seqid;
	var request = {
		method: method,
		handle: handle,
		params: args,
		id: seqid,
		jsonrpc: '2.0'
	};
	return new Promise(function(resolve, reject) {
		connection.pending[seqid] = {
			resolve: resolve,
			reject: reject
		};
		connection.ws.send(JSON.stringify(request));
	});
};
Connection.prototype.create = function(arg) {
	if (qSocks[arg.qType]) {
		return new qSocks[arg.qType](this, arg.qHandle);
	} else {
		return null;
	}
};
module.exports = qSocks;<commit_msg>Lowercase qsocks for npm<commit_after>var doc = require('./lib/doc');
var field = require('./lib/field');
var genericBookmark = require('./lib/GenericBookmark');
var genericDimension = require('./lib/GenericDimension');
var genericMeasure = require('./lib/GenericMeasure');
var genericObject = require('./lib/GenericObject');
var global = require('./lib/global');
var variable = require('./lib/variable');

var WebSocket = require('ws');
var Promise = require("es6-promise").Promise;

var qsocks = {
	Doc: doc,
	Field: field,
	GenericBookmark: genericBookmark,
	GenericDimension: genericDimension,
	GenericMeasure: genericMeasure,
	GenericObject: genericObject,
	Global: global,
	Variable: variable
};

function Connect(config) {
	var cfg = {};
	if (config) {
		cfg.mark = config.mark;
		cfg.appname = config.appname || false;
		cfg.host = config.host;
		cfg.origin = config.origin;
		cfg.isSecure = config.isSecure;
		cfg.rejectUnauthorized = config.rejectUnauthorized;
		cfg.headers = config.headers || {};
	}

	return new Promise(function(resolve, reject) {
		cfg.done = function(glob) {
			resolve(glob);
		};
		cfg.error = function(msg) {
			reject(msg);
		};
		new Connection(cfg);
	});
};

qSocks.Connect = Connect;

function Connection(config) {
	var mark = (config && config.mark) ? config.mark + ': ' : '';
	var host = (config && config.host) ? config.host : 'localhost';
	var port = host === 'localhost' ? ':4848' : '';
	var isSecure = (config && config.isSecure) ? 'wss://' : 'ws://'
	var error = config ? config.error : null;
	var done = config ? config.done : null;
	this.mark = mark;
	this.seqid = 0;
	this.pending = {};
	this.handles = {};
	var self = this;
	var suffix = config.appname ? '/sense/app/' + config.appname : '';

	this.ws = new WebSocket(isSecure + host + port + suffix, null, config);

	this.ws.onopen = function(ev) {
		if (done) {
			done.call(self, new qSocks.Global(self, -1));
		};
	};
	this.ws.onerror = function(ev) {
		if (error) {
			console.log(ev.message)
		}
		self.ws = null;
	};
	this.ws.onclose = function() {
		var unexpected = self.ws != null;
		var pending = self.pending[-99];
		delete self.pending[-99];
		if (pending) {
			pending.callback();
		} else if (unexpected) {
			if (error) {
				error();
			}
		}
		self.ws = null;
	};
	this.ws.onmessage = function(ev) {
		var text = ev.data;
		var msg = JSON.parse(text);
		console.log(msg)
		var pending = self.pending[msg.id];
		delete self.pending[msg.id];
		if (pending) {
			if (msg.result) {
				pending.resolve(msg.result);
			} else {
				pending.reject(msg.error);
			}
		}
	};
}
Connection.prototype.ask = function(handle, method, args) {
	var connection = this;
	if (!Array.isArray(args)) {
		var array = [];
		for (var ix in args) {
			array[ix] = args[ix];
		}
		args = array;
	}
	var seqid = ++connection.seqid;
	var request = {
		method: method,
		handle: handle,
		params: args,
		id: seqid,
		jsonrpc: '2.0'
	};
	return new Promise(function(resolve, reject) {
		connection.pending[seqid] = {
			resolve: resolve,
			reject: reject
		};
		connection.ws.send(JSON.stringify(request));
	});
};
Connection.prototype.create = function(arg) {
	if (qSocks[arg.qType]) {
		return new qSocks[arg.qType](this, arg.qHandle);
	} else {
		return null;
	}
};
module.exports = qsocks;<|endoftext|>


<commit_before>'use strict';

import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';

import createFragment from 'react-addons-create-fragment';
import PageView from './PageView';
import BreakView from './BreakView';

export default class PaginationBoxView extends Component {
  static propTypes = {
    pageCount             : PropTypes.number.isRequired,
    pageRangeDisplayed    : PropTypes.number.isRequired,
    marginPagesDisplayed  : PropTypes.number.isRequired,
    previousLabel         : PropTypes.node,
    nextLabel             : PropTypes.node,
    breakLabel            : PropTypes.node,
    hrefBuilder           : PropTypes.func,
    onPageChange          : PropTypes.func,
    initialPage           : PropTypes.number,
    forcePage             : PropTypes.number,
    disableInitialCallback: PropTypes.bool,
    containerClassName    : PropTypes.string,
    pageClassName         : PropTypes.string,
    pageLinkClassName     : PropTypes.string,
    activeClassName       : PropTypes.string,
    previousClassName     : PropTypes.string,
    nextClassName         : PropTypes.string,
    previousLinkClassName : PropTypes.string,
    nextLinkClassName     : PropTypes.string,
    disabledClassName     : PropTypes.string,
    breakClassName        : PropTypes.string
  };

  static defaultProps = {
    pageCount             : 10,
    pageRangeDisplayed    : 2,
    marginPagesDisplayed  : 3,
    activeClassName       : "selected",
    previousClassName     : "previous",
    nextClassName         : "next",
    previousLabel         : "Previous",
    nextLabel             : "Next",
    breakLabel            : "...",
    disabledClassName     : "disabled",
    disableInitialCallback: false
  };

  constructor(props) {
    super(props);

    this.state = {
      selected: props.initialPage ? props.initialPage :
                props.forcePage   ? props.forcePage :
                0
    };
  }

  componentDidMount() {
    // Call the callback with the initialPage item:
    if (typeof(this.props.initialPage) !== 'undefined' && !this.props.disableInitialCallback) {
      this.callCallback(this.props.initialPage);
    }
  }

  componentWillReceiveProps(nextProps) {
    if (typeof(nextProps.forcePage) !== 'undefined' && this.props.forcePage !== nextProps.forcePage) {
      this.setState({selected: nextProps.forcePage});
    }
  }

  handlePreviousPage = evt => {
    evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false);
    if (this.state.selected > 0) {
      this.handlePageSelected(this.state.selected - 1, evt);
    }
  };

  handleNextPage = evt => {
    evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false);
    if (this.state.selected < this.props.pageCount - 1) {
      this.handlePageSelected(this.state.selected + 1, evt);
    }
  };

  handlePageSelected = (selected, evt) => {
    evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false);

    if (this.state.selected === selected) return;

    this.setState({selected: selected});

    // Call the callback with the new selected item:
    this.callCallback(selected);
  };

  hrefBuilder(pageIndex) {
    if (this.props.hrefBuilder &&
      pageIndex !== this.state.selected &&
      pageIndex >= 0 &&
      pageIndex < this.props.pageCount
    ) {
      return this.props.hrefBuilder(pageIndex + 1);
    }
  }

  callCallback = (selectedItem) => {
    if (typeof(this.props.onPageChange) !== "undefined" &&
        typeof(this.props.onPageChange) === "function") {
      this.props.onPageChange({selected: selectedItem});
    }
  };

  getPageElement(index) {
    return <PageView
      onClick={this.handlePageSelected.bind(null, index)}
      selected={this.state.selected === index}
      pageClassName={this.props.pageClassName}
      pageLinkClassName={this.props.pageLinkClassName}
      activeClassName={this.props.activeClassName}
      href={this.hrefBuilder(index)}
      page={index + 1} />
  }

  pagination = () => {
    let items = {};

    if (this.props.pageCount <= this.props.pageRangeDisplayed) {

      for (let index = 0; index < this.props.pageCount; index++) {
        items['key' + index] = this.getPageElement(index);
      }

    } else {

      let leftSide  = (this.props.pageRangeDisplayed / 2);
      let rightSide = (this.props.pageRangeDisplayed - leftSide);

      if (this.state.selected > this.props.pageCount - this.props.pageRangeDisplayed / 2) {
        rightSide = this.props.pageCount - this.state.selected;
        leftSide  = this.props.pageRangeDisplayed - rightSide;
      }
      else if (this.state.selected < this.props.pageRangeDisplayed / 2) {
        leftSide  = this.state.selected;
        rightSide = this.props.pageRangeDisplayed - leftSide;
      }

      let index;
      let page;
      let breakView;

      for (index = 0; index < this.props.pageCount; index++) {

        page = index + 1;

        let pageView = this.getPageElement(index);

        if (page <= this.props.marginPagesDisplayed) {
          items['key' + index] = pageView;
          continue;
        }

        if (page > this.props.pageCount - this.props.marginPagesDisplayed) {
          items['key' + index] = pageView;
          continue;
        }

        if ((index >= this.state.selected - leftSide) && (index <= this.state.selected + rightSide)) {
          items['key' + index] = pageView;
          continue;
        }

        let keys            = Object.keys(items);
        let breakLabelKey   = keys[keys.length - 1];
        let breakLabelValue = items[breakLabelKey];

        if (this.props.breakLabel && breakLabelValue !== breakView) {
          breakView = (
            <BreakView
              breakLabel={this.props.breakLabel}
              breakClassName={this.props.breakClassName}
            />
          );

          items['key' + index] = breakView;
        }
      }
    }

    return items;
  };

  render() {
    let disabled = this.props.disabledClassName;

    const previousClasses = classNames(this.props.previousClassName,
                                       {[disabled]: this.state.selected === 0});

    const nextClasses = classNames(this.props.nextClassName,
                                   {[disabled]: this.state.selected === this.props.pageCount - 1});

    return (

            {this.props.previousLabel}

        {createFragment(this.pagination())}

            {this.props.nextLabel}

    );
  }
};
<commit_msg>Pass prop extraAriaContext to PageView<commit_after>'use strict';

import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';

import createFragment from 'react-addons-create-fragment';
import PageView from './PageView';
import BreakView from './BreakView';

export default class PaginationBoxView extends Component {
  static propTypes = {
    pageCount             : PropTypes.number.isRequired,
    pageRangeDisplayed    : PropTypes.number.isRequired,
    marginPagesDisplayed  : PropTypes.number.isRequired,
    previousLabel         : PropTypes.node,
    nextLabel             : PropTypes.node,
    breakLabel            : PropTypes.node,
    hrefBuilder           : PropTypes.func,
    onPageChange          : PropTypes.func,
    initialPage           : PropTypes.number,
    forcePage             : PropTypes.number,
    disableInitialCallback: PropTypes.bool,
    containerClassName    : PropTypes.string,
    pageClassName         : PropTypes.string,
    pageLinkClassName     : PropTypes.string,
    activeClassName       : PropTypes.string,
    previousClassName     : PropTypes.string,
    nextClassName         : PropTypes.string,
    previousLinkClassName : PropTypes.string,
    nextLinkClassName     : PropTypes.string,
    disabledClassName     : PropTypes.string,
    breakClassName        : PropTypes.string
  };

  static defaultProps = {
    pageCount             : 10,
    pageRangeDisplayed    : 2,
    marginPagesDisplayed  : 3,
    activeClassName       : "selected",
    previousClassName     : "previous",
    nextClassName         : "next",
    previousLabel         : "Previous",
    nextLabel             : "Next",
    breakLabel            : "...",
    disabledClassName     : "disabled",
    disableInitialCallback: false
  };

  constructor(props) {
    super(props);

    this.state = {
      selected: props.initialPage ? props.initialPage :
                props.forcePage   ? props.forcePage :
                0
    };
  }

  componentDidMount() {
    // Call the callback with the initialPage item:
    if (typeof(this.props.initialPage) !== 'undefined' && !this.props.disableInitialCallback) {
      this.callCallback(this.props.initialPage);
    }
  }

  componentWillReceiveProps(nextProps) {
    if (typeof(nextProps.forcePage) !== 'undefined' && this.props.forcePage !== nextProps.forcePage) {
      this.setState({selected: nextProps.forcePage});
    }
  }

  handlePreviousPage = evt => {
    evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false);
    if (this.state.selected > 0) {
      this.handlePageSelected(this.state.selected - 1, evt);
    }
  };

  handleNextPage = evt => {
    evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false);
    if (this.state.selected < this.props.pageCount - 1) {
      this.handlePageSelected(this.state.selected + 1, evt);
    }
  };

  handlePageSelected = (selected, evt) => {
    evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false);

    if (this.state.selected === selected) return;

    this.setState({selected: selected});

    // Call the callback with the new selected item:
    this.callCallback(selected);
  };

  hrefBuilder(pageIndex) {
    if (this.props.hrefBuilder &&
      pageIndex !== this.state.selected &&
      pageIndex >= 0 &&
      pageIndex < this.props.pageCount
    ) {
      return this.props.hrefBuilder(pageIndex + 1);
    }
  }

  callCallback = (selectedItem) => {
    if (typeof(this.props.onPageChange) !== "undefined" &&
        typeof(this.props.onPageChange) === "function") {
      this.props.onPageChange({selected: selectedItem});
    }
  };

  getPageElement(index) {
    return <PageView
      onClick={this.handlePageSelected.bind(null, index)}
      selected={this.state.selected === index}
      pageClassName={this.props.pageClassName}
      pageLinkClassName={this.props.pageLinkClassName}
      activeClassName={this.props.activeClassName}
      extraAriaContext={this.props.extraAriaContext}
      href={this.hrefBuilder(index)}
      page={index + 1} />
  }

  pagination = () => {
    let items = {};

    if (this.props.pageCount <= this.props.pageRangeDisplayed) {

      for (let index = 0; index < this.props.pageCount; index++) {
        items['key' + index] = this.getPageElement(index);
      }

    } else {

      let leftSide  = (this.props.pageRangeDisplayed / 2);
      let rightSide = (this.props.pageRangeDisplayed - leftSide);

      if (this.state.selected > this.props.pageCount - this.props.pageRangeDisplayed / 2) {
        rightSide = this.props.pageCount - this.state.selected;
        leftSide  = this.props.pageRangeDisplayed - rightSide;
      }
      else if (this.state.selected < this.props.pageRangeDisplayed / 2) {
        leftSide  = this.state.selected;
        rightSide = this.props.pageRangeDisplayed - leftSide;
      }

      let index;
      let page;
      let breakView;

      for (index = 0; index < this.props.pageCount; index++) {

        page = index + 1;

        let pageView = this.getPageElement(index);

        if (page <= this.props.marginPagesDisplayed) {
          items['key' + index] = pageView;
          continue;
        }

        if (page > this.props.pageCount - this.props.marginPagesDisplayed) {
          items['key' + index] = pageView;
          continue;
        }

        if ((index >= this.state.selected - leftSide) && (index <= this.state.selected + rightSide)) {
          items['key' + index] = pageView;
          continue;
        }

        let keys            = Object.keys(items);
        let breakLabelKey   = keys[keys.length - 1];
        let breakLabelValue = items[breakLabelKey];

        if (this.props.breakLabel && breakLabelValue !== breakView) {
          breakView = (
            <BreakView
              breakLabel={this.props.breakLabel}
              breakClassName={this.props.breakClassName}
            />
          );

          items['key' + index] = breakView;
        }
      }
    }

    return items;
  };

  render() {
    let disabled = this.props.disabledClassName;

    const previousClasses = classNames(this.props.previousClassName,
                                       {[disabled]: this.state.selected === 0});

    const nextClasses = classNames(this.props.nextClassName,
                                   {[disabled]: this.state.selected === this.props.pageCount - 1});

    return (

            {this.props.previousLabel}

        {createFragment(this.pagination())}

            {this.props.nextLabel}

    );
  }
};
<|endoftext|>


<commit_before>define([], function() {
	return {
		size: function() {
			var cs = getComputedStyle(this.domNode),
				bb = cs.getPropertyValue('box-sizing') === 'border-box',
				height = parseFloat(cs.getPropertyValue('height')),
				width = parseFloat(cs.getPropertyValue('width'));

			if (!bb) {
				height += parseFloat(cs.getPropertyValue('padding-top') || 0);
				height += parseFloat(cs.getPropertyValue('padding-bottom') || 0);
				height += parseFloat(cs.getPropertyValue('border-top-width') || 0);
				height += parseFloat(cs.getPropertyValue('border-bottom-width') || 0);
				width += parseFloat(cs.getPropertyValue('padding-left') || 0);
				width += parseFloat(cs.getPropertyValue('padding-right') || 0);
				width += parseFloat(cs.getPropertyValue('border-left-width') || 0);
				width += parseFloat(cs.getPropertyValue('border-right-width') || 0);
			}
			return {
				height: height,
				width: width
			};
		}
	};
});<commit_msg>Fix _WithSize<commit_after>define([], function() {
	return {
		size: function() {
			var cs = getComputedStyle(this.domNode),
				bb = cs.getPropertyValue('box-sizing') === 'border-box',
				height = cs.getPropertyValue('height'),
				width = cs.getPropertyValue('width');

			if (height === 'auto') {
				height = this.domNode.offsetHeight;
			} else {
				height = parseFloat(height);
				if (!bb) {
					height += parseFloat(cs.getPropertyValue('padding-top') || 0);
					height += parseFloat(cs.getPropertyValue('padding-bottom') || 0);
					height += parseFloat(cs.getPropertyValue('border-top-width') || 0);
					height += parseFloat(cs.getPropertyValue('border-bottom-width') || 0);
				}
			}

			if (width === 'auto') {
				width = this.domNode.offsetWidth;
			} else {
				width = parseFloat(width);
				if (!bb) {
					width += parseFloat(cs.getPropertyValue('padding-left') || 0);
					width += parseFloat(cs.getPropertyValue('padding-right') || 0);
					width += parseFloat(cs.getPropertyValue('border-left-width') || 0);
					width += parseFloat(cs.getPropertyValue('border-right-width') || 0);
				}
			}

			return {
				height: height,
				width: width
			};
		}
	};
});<|endoftext|>


<commit_before>/*
 * typeahead.js
 * https://github.com/twitter/typeahead
 * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
 */

var DropdownView = (function() {
  var html = {
        suggestionsList: ''
      },
      css = {
        suggestionsList: { display: 'block' },
        suggestion: { whiteSpace: 'nowrap', cursor: 'pointer' },
        suggestionChild: { whiteSpace: 'normal' }
      };

  // constructor
  // -----------

  function DropdownView(o) {
    utils.bindAll(this);

    this.isOpen = false;
    this.isEmpty = true;
    this.isMouseOverDropdown = false;

    this.$menu = $(o.menu)
    .on('mouseenter.tt', this._handleMouseenter)
    .on('mouseleave.tt', this._handleMouseleave)
    .on('click.tt', '.tt-suggestion', this._handleSelection)
    .on('mouseover.tt', '.tt-suggestion', this._handleMouseover);
  }

  utils.mixin(DropdownView.prototype, EventTarget, {
    // private methods
    // ---------------

    _handleMouseenter: function() {
      this.isMouseOverDropdown = true;
    },

    _handleMouseleave: function() {
      this.isMouseOverDropdown = false;
    },

    _handleMouseover: function($e) {
      var $suggestion = $($e.currentTarget);

      this._getSuggestions().removeClass('tt-is-under-cursor');
      $suggestion.addClass('tt-is-under-cursor');
    },

    _handleSelection: function($e) {
      var $suggestion = $($e.currentTarget);
      this.trigger('suggestionSelected', extractSuggestion($suggestion));
    },

    _show: function() {
      // can't use jQuery#show because $menu is a span element we want
      // display: block; not dislay: inline;
      this.$menu.css('display', 'block');
    },

    _hide: function() {
      this.$menu.hide();
    },

    _moveCursor: function(increment) {
      var $suggestions, $cur, nextIndex, $underCursor;

      // don't bother moving the cursor if the menu is closed or empty
      if (!this.isVisible()) {
        return;
      }

      $suggestions = this._getSuggestions();
      $cur = $suggestions.filter('.tt-is-under-cursor');

      $cur.removeClass('tt-is-under-cursor');

      // shifting before and after modulo to deal with -1 index of search input
      nextIndex = $suggestions.index($cur) + increment;
      nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1;

      if (nextIndex === -1) {
        this.trigger('cursorRemoved');

        return;
      }

      else if (nextIndex < -1) {
        // circle to last suggestion
        nextIndex = $suggestions.length - 1;
      }

      $underCursor = $suggestions.eq(nextIndex).addClass('tt-is-under-cursor');

      // in the case of scrollable overflow
      // make sure the cursor is visible in the menu
      this._ensureVisibility($underCursor);

      this.trigger('cursorMoved', extractSuggestion($underCursor));
    },

    _getSuggestions: function() {
      return this.$menu.find('.tt-suggestions > .tt-suggestion');
    },

    _ensureVisibility: function($el) {
      var menuHeight = this.$menu.height() +
            parseInt(this.$menu.css('paddingTop'), 10) +
            parseInt(this.$menu.css('paddingBottom'), 10),
          menuScrollTop = this.$menu.scrollTop(),
          elTop = $el.position().top,
          elBottom = elTop + $el.outerHeight(true);

      if (elTop < 0) {
        this.$menu.scrollTop(menuScrollTop + elTop);
      }

      else if (menuHeight < elBottom) {
        this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
      }
    },

    // public methods
    // --------------

    destroy: function() {
      this.$menu.off('.tt');

      this.$menu = null;
    },

    isVisible: function() {
      return this.isOpen && !this.isEmpty;
    },

    closeUnlessMouseIsOverDropdown: function() {
      // this helps detect the scenario a blur event has triggered
      // this function. we don't want to close the menu in that case
      // because it'll prevent the probable associated click event
      // from being fired
      if (!this.isMouseOverDropdown) {
        this.close();
      }
    },

    close: function() {
      if (this.isOpen) {
        this.isOpen = false;
        this._hide();

        this.$menu
        .find('.tt-suggestions > .tt-suggestion')
        .removeClass('tt-is-under-cursor');

        this.trigger('closed');
      }
    },

    open: function() {
      if (!this.isOpen) {
        this.isOpen = true;
        !this.isEmpty && this._show();

        this.trigger('opened');
      }
    },

    setLanguageDirection: function(dir) {
      var ltrCss = { left: '0', right: 'auto' },
          rtlCss = { left: 'auto', right:' 0' };

      dir === 'ltr' ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss);
    },

    moveCursorUp: function() {
      this._moveCursor(-1);
    },

    moveCursorDown: function() {
      this._moveCursor(+1);
    },

    getSuggestionUnderCursor: function() {
      var $suggestion = this._getSuggestions()
          .filter('.tt-is-under-cursor')
          .first();

      return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
    },

    getFirstSuggestion: function() {
      var $suggestion = this._getSuggestions().first();

      return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
    },

    renderSuggestions: function(dataset, suggestions) {
      var datasetClassName = 'tt-dataset-' + dataset.name,
          wrapper = '
%body
',
          compiledHtml,
          $suggestionsList,
          $dataset = this.$menu.find('.' + datasetClassName),
          elBuilder,
          fragment,
          $el;

      // first time rendering suggestions for this dataset
      if ($dataset.length === 0) {
        $suggestionsList = $(html.suggestionsList).css(css.suggestionsList);

        $dataset = $('

')
        .addClass(datasetClassName)
        .append(dataset.header)
        .append($suggestionsList)
        .append(dataset.footer)
        .appendTo(this.$menu);
      }

      // suggestions to be rendered
      if (suggestions.length > 0) {
        this.isEmpty = false;
        this.isOpen && this._show();

        elBuilder = document.createElement('div');
        fragment = document.createDocumentFragment();

        utils.each(suggestions, function(i, suggestion) {
          suggestion.dataset = dataset.name;
          compiledHtml = dataset.template(suggestion.datum);
          elBuilder.innerHTML = wrapper.replace('%body', compiledHtml);

          $el = $(elBuilder.firstChild)
          .css(css.suggestion)
          .data('suggestion', suggestion);

          $el.children().each(function() {
            $(this).css(css.suggestionChild);
          });

          fragment.appendChild($el[0]);
        });

        // show this dataset in case it was previously empty
        // and render the new suggestions
        $dataset.show().find('.tt-suggestions').html(fragment);
      }

      // no suggestions to render
      else {
        this.clearSuggestions(dataset.name);
      }

      this.trigger('suggestionsRendered');
    },

    clearSuggestions: function(datasetName) {
      var $datasets = datasetName ?
            this.$menu.find('.tt-dataset-' + datasetName) :
            this.$menu.find('[class^="tt-dataset-"]'),
          $suggestions = $datasets.find('.tt-suggestions');

      $datasets.hide();
      $suggestions.empty();

      if (this._getSuggestions().length === 0) {
        this.isEmpty = true;
        this._hide();
      }
    }
  });

  return DropdownView;

  // helper functions
  // ----------------

  function extractSuggestion($el) {
    return $el.data('suggestion');
  }
})();
<commit_msg>Reset boolean on close function, this fixes #242 and #259 - I have added a false boolean on close function. Basically this boolean is responsible for closing the dropdown or not via closeUnlessMouseIsOverDropdown function.<commit_after>/*
 * typeahead.js
 * https://github.com/twitter/typeahead
 * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
 */

var DropdownView = (function() {
  var html = {
        suggestionsList: ''
      },
      css = {
        suggestionsList: { display: 'block' },
        suggestion: { whiteSpace: 'nowrap', cursor: 'pointer' },
        suggestionChild: { whiteSpace: 'normal' }
      };

  // constructor
  // -----------

  function DropdownView(o) {
    utils.bindAll(this);

    this.isOpen = false;
    this.isEmpty = true;
    this.isMouseOverDropdown = false;

    this.$menu = $(o.menu)
    .on('mouseenter.tt', this._handleMouseenter)
    .on('mouseleave.tt', this._handleMouseleave)
    .on('click.tt', '.tt-suggestion', this._handleSelection)
    .on('mouseover.tt', '.tt-suggestion', this._handleMouseover);
  }

  utils.mixin(DropdownView.prototype, EventTarget, {
    // private methods
    // ---------------

    _handleMouseenter: function() {
      this.isMouseOverDropdown = true;
    },

    _handleMouseleave: function() {
      this.isMouseOverDropdown = false;
    },

    _handleMouseover: function($e) {
      var $suggestion = $($e.currentTarget);

      this._getSuggestions().removeClass('tt-is-under-cursor');
      $suggestion.addClass('tt-is-under-cursor');
    },

    _handleSelection: function($e) {
      var $suggestion = $($e.currentTarget);
      this.trigger('suggestionSelected', extractSuggestion($suggestion));
    },

    _show: function() {
      // can't use jQuery#show because $menu is a span element we want
      // display: block; not dislay: inline;
      this.$menu.css('display', 'block');
    },

    _hide: function() {
      this.$menu.hide();
    },

    _moveCursor: function(increment) {
      var $suggestions, $cur, nextIndex, $underCursor;

      // don't bother moving the cursor if the menu is closed or empty
      if (!this.isVisible()) {
        return;
      }

      $suggestions = this._getSuggestions();
      $cur = $suggestions.filter('.tt-is-under-cursor');

      $cur.removeClass('tt-is-under-cursor');

      // shifting before and after modulo to deal with -1 index of search input
      nextIndex = $suggestions.index($cur) + increment;
      nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1;

      if (nextIndex === -1) {
        this.trigger('cursorRemoved');

        return;
      }

      else if (nextIndex < -1) {
        // circle to last suggestion
        nextIndex = $suggestions.length - 1;
      }

      $underCursor = $suggestions.eq(nextIndex).addClass('tt-is-under-cursor');

      // in the case of scrollable overflow
      // make sure the cursor is visible in the menu
      this._ensureVisibility($underCursor);

      this.trigger('cursorMoved', extractSuggestion($underCursor));
    },

    _getSuggestions: function() {
      return this.$menu.find('.tt-suggestions > .tt-suggestion');
    },

    _ensureVisibility: function($el) {
      var menuHeight = this.$menu.height() +
            parseInt(this.$menu.css('paddingTop'), 10) +
            parseInt(this.$menu.css('paddingBottom'), 10),
          menuScrollTop = this.$menu.scrollTop(),
          elTop = $el.position().top,
          elBottom = elTop + $el.outerHeight(true);

      if (elTop < 0) {
        this.$menu.scrollTop(menuScrollTop + elTop);
      }

      else if (menuHeight < elBottom) {
        this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
      }
    },

    // public methods
    // --------------

    destroy: function() {
      this.$menu.off('.tt');

      this.$menu = null;
    },

    isVisible: function() {
      return this.isOpen && !this.isEmpty;
    },

    closeUnlessMouseIsOverDropdown: function() {
      // this helps detect the scenario a blur event has triggered
      // this function. we don't want to close the menu in that case
      // because it'll prevent the probable associated click event
      // from being fired
      if (!this.isMouseOverDropdown) {
        this.close();
      }
    },

    close: function() {
      if (this.isOpen) {
        this.isOpen = false;
        this.isMouseOverDropdown = false;
        this._hide();

        this.$menu
        .find('.tt-suggestions > .tt-suggestion')
        .removeClass('tt-is-under-cursor');

        this.trigger('closed');
      }
    },

    open: function() {
      if (!this.isOpen) {
        this.isOpen = true;
        !this.isEmpty && this._show();

        this.trigger('opened');
      }
    },

    setLanguageDirection: function(dir) {
      var ltrCss = { left: '0', right: 'auto' },
          rtlCss = { left: 'auto', right:' 0' };

      dir === 'ltr' ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss);
    },

    moveCursorUp: function() {
      this._moveCursor(-1);
    },

    moveCursorDown: function() {
      this._moveCursor(+1);
    },

    getSuggestionUnderCursor: function() {
      var $suggestion = this._getSuggestions()
          .filter('.tt-is-under-cursor')
          .first();

      return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
    },

    getFirstSuggestion: function() {
      var $suggestion = this._getSuggestions().first();

      return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
    },

    renderSuggestions: function(dataset, suggestions) {
      var datasetClassName = 'tt-dataset-' + dataset.name,
          wrapper = '
%body
',
          compiledHtml,
          $suggestionsList,
          $dataset = this.$menu.find('.' + datasetClassName),
          elBuilder,
          fragment,
          $el;

      // first time rendering suggestions for this dataset
      if ($dataset.length === 0) {
        $suggestionsList = $(html.suggestionsList).css(css.suggestionsList);

        $dataset = $('

')
        .addClass(datasetClassName)
        .append(dataset.header)
        .append($suggestionsList)
        .append(dataset.footer)
        .appendTo(this.$menu);
      }

      // suggestions to be rendered
      if (suggestions.length > 0) {
        this.isEmpty = false;
        this.isOpen && this._show();

        elBuilder = document.createElement('div');
        fragment = document.createDocumentFragment();

        utils.each(suggestions, function(i, suggestion) {
          suggestion.dataset = dataset.name;
          compiledHtml = dataset.template(suggestion.datum);
          elBuilder.innerHTML = wrapper.replace('%body', compiledHtml);

          $el = $(elBuilder.firstChild)
          .css(css.suggestion)
          .data('suggestion', suggestion);

          $el.children().each(function() {
            $(this).css(css.suggestionChild);
          });

          fragment.appendChild($el[0]);
        });

        // show this dataset in case it was previously empty
        // and render the new suggestions
        $dataset.show().find('.tt-suggestions').html(fragment);
      }

      // no suggestions to render
      else {
        this.clearSuggestions(dataset.name);
      }

      this.trigger('suggestionsRendered');
    },

    clearSuggestions: function(datasetName) {
      var $datasets = datasetName ?
            this.$menu.find('.tt-dataset-' + datasetName) :
            this.$menu.find('[class^="tt-dataset-"]'),
          $suggestions = $datasets.find('.tt-suggestions');

      $datasets.hide();
      $suggestions.empty();

      if (this._getSuggestions().length === 0) {
        this.isEmpty = true;
        this._hide();
      }
    }
  });

  return DropdownView;

  // helper functions
  // ----------------

  function extractSuggestion($el) {
    return $el.data('suggestion');
  }
})();
<|endoftext|>


<commit_before>var Timer = require('../util/timer');
var Thread = require('./thread');
var execute = require('./execute.js');

function Sequencer (runtime) {
    /**
     * A utility timer for timing thread sequencing.
     * @type {!Timer}
     */
    this.timer = new Timer();

    /**
     * Reference to the runtime owning this sequencer.
     * @type {!Runtime}
     */
    this.runtime = runtime;
}

/**
 * The sequencer does as much work as it can within WORK_TIME milliseconds,
 * then yields. This is essentially a rate-limiter for blocks.
 * In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
 * @const {!number}
 */
Sequencer.WORK_TIME = 10;

/**
 * Step through all threads in `this.threads`, running them in order.
 * @param {Array.<Thread>} threads List of which threads to step.
 * @return {Array.<Thread>} All threads which have finished in this iteration.
 */
Sequencer.prototype.stepThreads = function (threads) {
    // Start counting toward WORK_TIME
    this.timer.start();
    // List of threads which have been killed by this step.
    var inactiveThreads = [];
    // If all of the threads are yielding, we should yield.
    var numYieldingThreads = 0;
    // Clear all yield statuses that were for the previous frame.
    for (var t = 0; t < threads.length; t++) {
        if (threads[t].status === Thread.STATUS_YIELD_FRAME) {
            threads[t].setStatus(Thread.STATUS_RUNNING);
        }
    }

    // While there are still threads to run and we are within WORK_TIME,
    // continue executing threads.
    while (threads.length > 0 &&
           threads.length > numYieldingThreads &&
           this.timer.timeElapsed() < Sequencer.WORK_TIME) {
        // New threads at the end of the iteration.
        var newThreads = [];
        // Reset yielding thread count.
        numYieldingThreads = 0;
        // Attempt to run each thread one time
        for (var i = 0; i < threads.length; i++) {
            var activeThread = threads[i];
            if (activeThread.status === Thread.STATUS_RUNNING) {
                // Normal-mode thread: step.
                this.startThread(activeThread);
            } else if (activeThread.status === Thread.STATUS_YIELD ||
                       activeThread.status === Thread.STATUS_YIELD_FRAME) {
                // Yielding thread: do nothing for this step.
                numYieldingThreads++;
            }
            if (activeThread.stack.length === 0 &&
                activeThread.status === Thread.STATUS_DONE) {
                // Finished with this thread - tell runtime to clean it up.
                inactiveThreads.push(activeThread);            } else {
                // Keep this thead in the loop.
                newThreads.push(activeThread);
            }
        }
        // Effectively filters out threads that have stopped.
        threads = newThreads;
    }
    return inactiveThreads;
};
/**
 * Step the requested thread
 * @param {!Thread} thread Thread object to step
 */
Sequencer.prototype.startThread = function (thread) {
    var currentBlockId = thread.peekStack();
    if (!currentBlockId) {
        // A "null block" - empty branch.
        // Yield for the frame.
        thread.popStack();
        thread.setStatus(Thread.STATUS_YIELD_FRAME);        return;
    }
    // Execute the current block
    execute(this, thread);
    // If the block executed without yielding and without doing control flow,
    // move to done.
    if (thread.status === Thread.STATUS_RUNNING &&
        thread.peekStack() === currentBlockId) {
        this.proceedThread(thread);
    }
};
/**
 * Step a thread into a block's branch.
 * @param {!Thread} thread Thread object to step to branch. * @param {Number} branchNum Which branch to step to (i.e., 1, 2).
 */
Sequencer.prototype.stepToBranch = function (thread, branchNum) {
    if (!branchNum) {
        branchNum = 1;
    }
    var currentBlockId = thread.peekStack();
    var branchId = thread.target.blocks.getBranch(
        currentBlockId,
        branchNum
    );
    if (branchId) {
        // Push branch ID to the thread's stack.
        thread.pushStack(branchId);
    } else {
        // Push null, so we come back to the current block.
        thread.pushStack(null);
    }
};

/**
 * Step a procedure.
 * @param {!Thread} thread Thread object to step to procedure.
 * @param {!string} procedureName Name of procedure defined in this target.
 */
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
    var definition = thread.target.blocks.getProcedureDefinition(procedureName);
    thread.pushStack(definition);
};

/**
 * Step a thread into an input reporter, and manage its status appropriately.
 * @param {!Thread} thread Thread object to step to reporter.
 * @param {!string} blockId ID of reporter block.
 * @param {!string} inputName Name of input on parent block.
 * @return {boolean} True if yielded, false if it finished immediately.
 */
Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) {
    var currentStackFrame = thread.peekStackFrame();
    // Push to the stack to evaluate the reporter block.
    thread.pushStack(blockId);
    // Save name of input for `Thread.pushReportedValue`.
    currentStackFrame.waitingReporter = inputName;
    // Actually execute the block.
    this.startThread(thread);
    // If a reporter yielded, caller must wait for it to unyield.
    // The value will be populated once the reporter unyields,
    // and passed up to the currentStackFrame on next execution.
    return thread.status === Thread.STATUS_YIELD;
};

/**
 * Finish stepping a thread and proceed it to the next block.
 * @param {!Thread} thread Thread object to proceed.
 */
Sequencer.prototype.proceedThread = function (thread) {
    var currentBlockId = thread.peekStack();
    // Mark the status as done and proceed to the next block.
    // Pop from the stack - finished this level of execution.
    thread.popStack();
    // Push next connected block, if there is one.
    var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
    if (nextBlockId) {
        thread.pushStack(nextBlockId);
    }
    // If we can't find a next block to run, mark the thread as done.
    if (!thread.peekStack()) {
        thread.setStatus(Thread.STATUS_DONE);
    }
};

/**
 * Retire a thread in the middle, without considering further blocks.
 * @param {!Thread} thread Thread object to retire.
 */
Sequencer.prototype.retireThread = function (thread) {
    thread.stack = [];
    thread.stackFrame = [];
    thread.requestScriptGlowInFrame = false;
    thread.setStatus(Thread.STATUS_DONE);
};

module.exports = Sequencer;
<commit_msg>Fix Error<commit_after>var Timer = require('../util/timer');
var Thread = require('./thread');
var execute = require('./execute.js');

function Sequencer (runtime) {
    /**
     * A utility timer for timing thread sequencing.
     * @type {!Timer}
     */
    this.timer = new Timer();

    /**
     * Reference to the runtime owning this sequencer.
     * @type {!Runtime}
     */
    this.runtime = runtime;
}

/**
 * The sequencer does as much work as it can within WORK_TIME milliseconds,
 * then yields. This is essentially a rate-limiter for blocks.
 * In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
 * @const {!number}
 */
Sequencer.WORK_TIME = 10;

/**
 * Step through all threads in `this.threads`, running them in order.
 * @param {Array.<Thread>} threads List of which threads to step.
 * @return {Array.<Thread>} All threads which have finished in this iteration.
 */
Sequencer.prototype.stepThreads = function (threads) {
    // Start counting toward WORK_TIME
    this.timer.start();
    // List of threads which have been killed by this step.
    var inactiveThreads = [];
    // If all of the threads are yielding, we should yield.
    var numYieldingThreads = 0;
    // Clear all yield statuses that were for the previous frame.
    for (var t = 0; t < threads.length; t++) {
        if (threads[t].status === Thread.STATUS_YIELD_FRAME) {
            threads[t].setStatus(Thread.STATUS_RUNNING);
        }
    }

    // While there are still threads to run and we are within WORK_TIME,
    // continue executing threads.
    while (threads.length > 0 &&
           threads.length > numYieldingThreads &&
           this.timer.timeElapsed() < Sequencer.WORK_TIME) {
        // New threads at the end of the iteration.
        var newThreads = [];
        // Reset yielding thread count.
        numYieldingThreads = 0;
        // Attempt to run each thread one time
        for (var i = 0; i < threads.length; i++) {
            var activeThread = threads[i];
            if (activeThread.status === Thread.STATUS_RUNNING) {
                // Normal-mode thread: step.
                this.startThread(activeThread);
            } else if (activeThread.status === Thread.STATUS_YIELD ||
                       activeThread.status === Thread.STATUS_YIELD_FRAME) {
                // Yielding thread: do nothing for this step.
                numYieldingThreads++;
            }
            if (activeThread.stack.length === 0 &&
                activeThread.status === Thread.STATUS_DONE) {
                // Finished with this thread - tell runtime to clean it up.

                inactiveThreads.push(activeThread);            } else {
                // Keep this thead in the loop.
                newThreads.push(activeThread);
            }
        }
        // Effectively filters out threads that have stopped.
        threads = newThreads;
    }
    return inactiveThreads;
};
/** * Step the requested thread
 * @param {!Thread} thread Thread object to step
 */
Sequencer.prototype.startThread = function (thread) {
    var currentBlockId = thread.peekStack();
    if (!currentBlockId) {
        // A "null block" - empty branch.
        // Yield for the frame.
        thread.popStack();
        thread.setStatus(Thread.STATUS_YIELD_FRAME);        return;
    }
    // Execute the current block
    execute(this, thread);
    // If the block executed without yielding and without doing control flow,
    // move to done.
    if (thread.status === Thread.STATUS_RUNNING &&
        thread.peekStack() === currentBlockId) {
        this.proceedThread(thread);
    }
};
/**
 * Step a thread into a block's branch.
 * @param {!Thread} thread Thread object to step to branch.
 * @param {Number} branchNum Which branch to step to (i.e., 1, 2).
 */
Sequencer.prototype.stepToBranch = function (thread, branchNum) {
    if (!branchNum) {
        branchNum = 1;
    }
    var currentBlockId = thread.peekStack();
    var branchId = thread.target.blocks.getBranch(
        currentBlockId,
        branchNum
    );
    if (branchId) {
        // Push branch ID to the thread's stack.
        thread.pushStack(branchId);
    } else {
        // Push null, so we come back to the current block.
        thread.pushStack(null);
    }
};

/**
 * Step a procedure.
 * @param {!Thread} thread Thread object to step to procedure.
 * @param {!string} procedureName Name of procedure defined in this target.
 */
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
    var definition = thread.target.blocks.getProcedureDefinition(procedureName);
    thread.pushStack(definition);
};

/**
 * Step a thread into an input reporter, and manage its status appropriately.
 * @param {!Thread} thread Thread object to step to reporter.
 * @param {!string} blockId ID of reporter block.
 * @param {!string} inputName Name of input on parent block.
 * @return {boolean} True if yielded, false if it finished immediately.
 */
Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) {
    var currentStackFrame = thread.peekStackFrame();
    // Push to the stack to evaluate the reporter block.
    thread.pushStack(blockId);
    // Save name of input for `Thread.pushReportedValue`.
    currentStackFrame.waitingReporter = inputName;
    // Actually execute the block.
    this.startThread(thread);
    // If a reporter yielded, caller must wait for it to unyield.
    // The value will be populated once the reporter unyields,
    // and passed up to the currentStackFrame on next execution.
    return thread.status === Thread.STATUS_YIELD;
};

/**
 * Finish stepping a thread and proceed it to the next block.
 * @param {!Thread} thread Thread object to proceed.
 */
Sequencer.prototype.proceedThread = function (thread) {
    var currentBlockId = thread.peekStack();
    // Mark the status as done and proceed to the next block.
    // Pop from the stack - finished this level of execution.
    thread.popStack();
    // Push next connected block, if there is one.
    var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
    if (nextBlockId) {
        thread.pushStack(nextBlockId);
    }
    // If we can't find a next block to run, mark the thread as done.
    if (!thread.peekStack()) {
        thread.setStatus(Thread.STATUS_DONE);
    }
};

/**
 * Retire a thread in the middle, without considering further blocks.
 * @param {!Thread} thread Thread object to retire.
 */
Sequencer.prototype.retireThread = function (thread) {
    thread.stack = [];
    thread.stackFrame = [];
    thread.requestScriptGlowInFrame = false;
    thread.setStatus(Thread.STATUS_DONE);
};

module.exports = Sequencer;
<|endoftext|>


<commit_before>var Timer = require('../util/timer');
var Thread = require('./thread');
var execute = require('./execute.js');

function Sequencer (runtime) {
    /**
     * A utility timer for timing thread sequencing.
     * @type {!Timer}
     */
    this.timer = new Timer();

    /**
     * Reference to the runtime owning this sequencer.
     * @type {!Runtime}
     */
    this.runtime = runtime;
}

/**
 * The sequencer does as much work as it can within WORK_TIME milliseconds,
 * then yields. This is essentially a rate-limiter for blocks.
 * In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
 * @const {!number}
 */
Sequencer.WORK_TIME = 10;

/**
 * Step through all threads in `this.threads`, running them in order.
 * @param {Array.<Thread>} threads List of which threads to step.
 * @return {Array.<Thread>} All threads which have finished in this iteration.
 */
Sequencer.prototype.stepThreads = function (threads) {
    // Start counting toward WORK_TIME
    this.timer.start();
    // Check for a blocking thread.
    var blockingThread = this.getBlockingThread_(threads);
    if (blockingThread) {
        // Attempt to resolve any timeouts, but otherwise stop stepping.
        blockingThread.resolveTimeouts();
        return [];
    }
    // List of threads which have been killed by this step.
    var inactiveThreads = [];
    // If all of the threads are yielding, we should yield.
    var numYieldingThreads = 0;
    // While there are still threads to run and we are within WORK_TIME,
    // continue executing threads.
    while (threads.length > 0 &&
           threads.length > numYieldingThreads &&
           this.timer.timeElapsed() < Sequencer.WORK_TIME) {
        // New threads at the end of the iteration.
        var newThreads = [];
        // Reset yielding thread count.
        numYieldingThreads = 0;
        // Attempt to run each thread one time
        for (var i = 0; i < threads.length; i++) {
            var activeThread = threads[i];
            if (activeThread.status === Thread.STATUS_RUNNING) {
                // Normal-mode thread: step.
                this.startThread(activeThread);
            } else if (activeThread.status === Thread.STATUS_YIELD) {
                // Yield-mode thread: resolve timers.
                activeThread.resolveTimeouts();
                if (activeThread.status === Thread.STATUS_YIELD) {
                    // Still yielding.
                    numYieldingThreads++;
                }
            } else if (activeThread.status === Thread.STATUS_DONE) {
                // Moved to a done state - finish up
                activeThread.status = Thread.STATUS_RUNNING;
                // @todo Deal with the return value
            }
            // Has the thread gone into "blocking" mode? If so, stop stepping.
            if (activeThread.status === Thread.STATUS_YIELD_BLOCK) {
                return inactiveThreads;
            }
            if (activeThread.stack.length === 0 &&
                activeThread.status === Thread.STATUS_DONE) {
                // Finished with this thread - tell runtime to clean it up.
                inactiveThreads.push(activeThread);
            } else {
                // Keep this thead in the loop.
                newThreads.push(activeThread);
            }
        }
        // Effectively filters out threads that have stopped.
        threads = newThreads;
    }
    return inactiveThreads;
};

/**
 * Return the thread blocking all other threads, if one exists.
 * If not, return false.
 * @param {Array.<Thread>} threads List of which threads to check.
 * @return {?Thread} The blocking thread if one exists.
 */
Sequencer.prototype.getBlockingThread_ = function (threads) {
    for (var i = 0; i < threads.length; i++) {
        if (threads[i].status === Thread.STATUS_YIELD_BLOCK) {
            return threads[i];
        }
    }
    return false;
};

/**
 * Step the requested thread
 * @param {!Thread} thread Thread object to step
 */
Sequencer.prototype.startThread = function (thread) {
    var currentBlockId = thread.peekStack();
    if (!currentBlockId) {
        // A "null block" - empty substack. Pop the stack.
        thread.popStack();
        thread.status = Thread.STATUS_DONE;
        return;
    }
    // Start showing run feedback in the editor.
    this.runtime.glowBlock(currentBlockId, true);

    // Execute the current block
    execute(this, thread);

    // If the block executed without yielding and without doing control flow,
    // move to done.
    if (thread.status === Thread.STATUS_RUNNING &&
        thread.peekStack() === currentBlockId) {
        this.proceedThread(thread);
    }
};

/**
 * Step a thread into a block's substack.
 * @param {!Thread} thread Thread object to step to substack.
 * @param {Number} substackNum Which substack to step to (i.e., 1, 2).
 */
Sequencer.prototype.stepToSubstack = function (thread, substackNum) {
    if (!substackNum) {
        substackNum = 1;
    }
    var currentBlockId = thread.peekStack();
    var substackId = this.runtime.blocks.getSubstack(
        currentBlockId,
        substackNum
    );
    if (substackId) {
        // Push substack ID to the thread's stack.
        thread.pushStack(substackId);
    } else {
        // Push null, so we come back to the current block.
        thread.pushStack(null);
    }
};

/**
 * Step a thread into an input reporter, and manage its status appropriately.
 * @param {!Thread} thread Thread object to step to reporter.
 * @param {!string} blockId ID of reporter block.
 * @param {!string} inputName Name of input on parent block.
 * @return {boolean} True if yielded, false if it finished immediately.
 */
Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) {
    var currentStackFrame = thread.peekStackFrame();
    // Push to the stack to evaluate the reporter block.
    thread.pushStack(blockId);
    // Save name of input for `Thread.pushReportedValue`.
    currentStackFrame.waitingReporter = inputName;
    // Actually execute the block.
    this.startThread(thread);
    if (thread.status === Thread.STATUS_YIELD) {
        // Reporter yielded; caller must wait for it to unyield.
        // The value will be populated once the reporter unyields,
        // and passed up to the currentStackFrame on next execution.
        return true;
    } else if (thread.status === Thread.STATUS_DONE) {
        // Reporter finished, mark the thread as running.
        thread.status = Thread.STATUS_RUNNING;
        return false;
    }
};

/**
 * Finish stepping a thread and proceed it to the next block.
 * @param {!Thread} thread Thread object to proceed.
 */
Sequencer.prototype.proceedThread = function (thread) {
    var currentBlockId = thread.peekStack();
    // Mark the status as done and proceed to the next block.
    this.runtime.glowBlock(currentBlockId, false);
    thread.status = Thread.STATUS_DONE;
    // Pop from the stack - finished this level of execution.
    thread.popStack();
    // Push next connected block, if there is one.
    var nextBlockId = this.runtime.blocks.getNextBlock(currentBlockId);
    if (nextBlockId) {
        thread.pushStack(nextBlockId);
    }
    // Pop from the stack until we have a next block.
    while (thread.peekStack() === null && thread.stack.length > 0) {
        thread.popStack();
    }
};

module.exports = Sequencer;
<commit_msg>getBlockingThread_ returns null when none available.<commit_after>var Timer = require('../util/timer');
var Thread = require('./thread');
var execute = require('./execute.js');

function Sequencer (runtime) {
    /**
     * A utility timer for timing thread sequencing.
     * @type {!Timer}
     */
    this.timer = new Timer();

    /**
     * Reference to the runtime owning this sequencer.
     * @type {!Runtime}
     */
    this.runtime = runtime;
}

/**
 * The sequencer does as much work as it can within WORK_TIME milliseconds,
 * then yields. This is essentially a rate-limiter for blocks.
 * In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
 * @const {!number}
 */
Sequencer.WORK_TIME = 10;

/**
 * Step through all threads in `this.threads`, running them in order.
 * @param {Array.<Thread>} threads List of which threads to step.
 * @return {Array.<Thread>} All threads which have finished in this iteration.
 */
Sequencer.prototype.stepThreads = function (threads) {
    // Start counting toward WORK_TIME
    this.timer.start();
    // Check for a blocking thread.
    var blockingThread = this.getBlockingThread_(threads);
    if (blockingThread) {
        // Attempt to resolve any timeouts, but otherwise stop stepping.
        blockingThread.resolveTimeouts();
        return [];
    }
    // List of threads which have been killed by this step.
    var inactiveThreads = [];
    // If all of the threads are yielding, we should yield.
    var numYieldingThreads = 0;
    // While there are still threads to run and we are within WORK_TIME,
    // continue executing threads.
    while (threads.length > 0 &&
           threads.length > numYieldingThreads &&
           this.timer.timeElapsed() < Sequencer.WORK_TIME) {
        // New threads at the end of the iteration.
        var newThreads = [];
        // Reset yielding thread count.
        numYieldingThreads = 0;
        // Attempt to run each thread one time
        for (var i = 0; i < threads.length; i++) {
            var activeThread = threads[i];
            if (activeThread.status === Thread.STATUS_RUNNING) {
                // Normal-mode thread: step.
                this.startThread(activeThread);
            } else if (activeThread.status === Thread.STATUS_YIELD) {
                // Yield-mode thread: resolve timers.
                activeThread.resolveTimeouts();
                if (activeThread.status === Thread.STATUS_YIELD) {
                    // Still yielding.
                    numYieldingThreads++;
                }
            } else if (activeThread.status === Thread.STATUS_DONE) {
                // Moved to a done state - finish up
                activeThread.status = Thread.STATUS_RUNNING;
                // @todo Deal with the return value
            }
            // Has the thread gone into "blocking" mode? If so, stop stepping.
            if (activeThread.status === Thread.STATUS_YIELD_BLOCK) {
                return inactiveThreads;
            }
            if (activeThread.stack.length === 0 &&
                activeThread.status === Thread.STATUS_DONE) {
                // Finished with this thread - tell runtime to clean it up.
                inactiveThreads.push(activeThread);
            } else {
                // Keep this thead in the loop.
                newThreads.push(activeThread);
            }
        }
        // Effectively filters out threads that have stopped.
        threads = newThreads;
    }
    return inactiveThreads;
};

/**
 * Return the thread blocking all other threads, if one exists.
 * @param {Array.<Thread>} threads List of which threads to check.
 * @return {?Thread} The blocking thread if one exists.
 */
Sequencer.prototype.getBlockingThread_ = function (threads) {
    for (var i = 0; i < threads.length; i++) {
        if (threads[i].status === Thread.STATUS_YIELD_BLOCK) {
            return threads[i];
        }
    }
    return null;
};

/**
 * Step the requested thread
 * @param {!Thread} thread Thread object to step
 */
Sequencer.prototype.startThread = function (thread) {
    var currentBlockId = thread.peekStack();
    if (!currentBlockId) {
        // A "null block" - empty substack. Pop the stack.
        thread.popStack();
        thread.status = Thread.STATUS_DONE;
        return;
    }
    // Start showing run feedback in the editor.
    this.runtime.glowBlock(currentBlockId, true);

    // Execute the current block
    execute(this, thread);

    // If the block executed without yielding and without doing control flow,
    // move to done.
    if (thread.status === Thread.STATUS_RUNNING &&
        thread.peekStack() === currentBlockId) {
        this.proceedThread(thread);
    }
};

/**
 * Step a thread into a block's substack.
 * @param {!Thread} thread Thread object to step to substack.
 * @param {Number} substackNum Which substack to step to (i.e., 1, 2).
 */
Sequencer.prototype.stepToSubstack = function (thread, substackNum) {
    if (!substackNum) {
        substackNum = 1;
    }
    var currentBlockId = thread.peekStack();
    var substackId = this.runtime.blocks.getSubstack(
        currentBlockId,
        substackNum
    );
    if (substackId) {
        // Push substack ID to the thread's stack.
        thread.pushStack(substackId);
    } else {
        // Push null, so we come back to the current block.
        thread.pushStack(null);
    }
};

/**
 * Step a thread into an input reporter, and manage its status appropriately.
 * @param {!Thread} thread Thread object to step to reporter.
 * @param {!string} blockId ID of reporter block.
 * @param {!string} inputName Name of input on parent block.
 * @return {boolean} True if yielded, false if it finished immediately.
 */
Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) {
    var currentStackFrame = thread.peekStackFrame();
    // Push to the stack to evaluate the reporter block.
    thread.pushStack(blockId);
    // Save name of input for `Thread.pushReportedValue`.
    currentStackFrame.waitingReporter = inputName;
    // Actually execute the block.
    this.startThread(thread);
    if (thread.status === Thread.STATUS_YIELD) {
        // Reporter yielded; caller must wait for it to unyield.
        // The value will be populated once the reporter unyields,
        // and passed up to the currentStackFrame on next execution.
        return true;
    } else if (thread.status === Thread.STATUS_DONE) {
        // Reporter finished, mark the thread as running.
        thread.status = Thread.STATUS_RUNNING;
        return false;
    }
};

/**
 * Finish stepping a thread and proceed it to the next block.
 * @param {!Thread} thread Thread object to proceed.
 */
Sequencer.prototype.proceedThread = function (thread) {
    var currentBlockId = thread.peekStack();
    // Mark the status as done and proceed to the next block.
    this.runtime.glowBlock(currentBlockId, false);
    thread.status = Thread.STATUS_DONE;
    // Pop from the stack - finished this level of execution.
    thread.popStack();
    // Push next connected block, if there is one.
    var nextBlockId = this.runtime.blocks.getNextBlock(currentBlockId);
    if (nextBlockId) {
        thread.pushStack(nextBlockId);
    }
    // Pop from the stack until we have a next block.
    while (thread.peekStack() === null && thread.stack.length > 0) {
        thread.popStack();
    }
};

module.exports = Sequencer;
<|endoftext|>