<commit_before>import _ from 'underscore';
import app from '../../../../app';
import { isScrolledIntoView } from '../../../../utils/dom';
import { getSocket, getCurrentConnection } from '../../../../utils/serverConnect';
import { openSimpleMessage } from '../../SimpleMessage';
import { launchSettingsModal } from '../../../../utils/modalManager';
import loadTemplate from '../../../../utils/loadTemplate';
import BaseVw from '../../../baseVw';
import Transaction from './Transaction';
import TransactionFetchState from './TransactionFetchState';
import PopInMessage, { buildRefreshAlertMessage } from '../../../components/PopInMessage';

export default class extends BaseVw {
  constructor(options = {}) {
    const opts = {
      fetchOnInit: true,
      // If not fetching on init, you may want to pass in the count of transactions
      // that were returned by the first fetch which is used to determine if all the
      // pages have been fetched.
      countAtFirstFetch: undefined,
      // If there are any existing bump fee attempts that you want shuttled into the
      // individual transaction views, please provide an indexed object (indexed by txid)
      // of them here.
      bumpFeeXhrs: undefined,
      ...options,
    };

    super(options);
    this.options = opts;

    if (!this.collection) {
      throw new Error('Please provide a Transactions collection.');
    }

    if (!opts.$scrollContainer || !opts.$scrollContainer.length) {
      throw new Error('Please provide a jQuery object containing the scrollable element ' +
        'this view is in.');
    }

    this.transactionViews = [];
    this.fetchFailed = false;
    this.fetchErrorMessage = '';
    this.newTransactionCount = 0;
    this.popInTimeouts = [];
    this.coinType = this.collection.options.coinType;
    this.countAtFirstFetch = opts.countAtFirstFetch;

    this.listenTo(this.collection, 'update', (cl, clUpdateOpts) => {
      if (clUpdateOpts.changes.added.length) {
        // Expecting either a single new transactions on top or a page
        // of transactions on the bottom.
        if (clUpdateOpts.changes.added.length === this.collection.length ||
          clUpdateOpts.changes.added[clUpdateOpts.changes.added.length - 1] ===
            this.collection.at(this.collection.length - 1)) {
          // It's a page of transactions at the bottom
          this.renderTransactions(clUpdateOpts.changes.added, 'append');
        } else {
          // New transaction at top
          this.renderTransactions(clUpdateOpts.changes.added, 'prepend');
        }
      }
    });

    this.listenTo(app.settings, 'change:localCurrency', () => {
      this.renderTransactions(this.collection.models, 'replace');
    });

    this.listenTo(app.localSettings, 'change:bitcoinUnit', () => {
      this.renderTransactions(this.collection.models, 'replace');
    });

    const serverSocket = getSocket();

    if (serverSocket) {
      this.listenTo(serverSocket, 'message', e => {
        // "wallet" sockets come for new transactions and when a transaction gets it's
        // first confirmation.
        if (e.jsonData.wallet && e.jsonData.wallet.wallet === this.coinType) {
          const transaction = this.collection.get(e.jsonData.wallet.txid);

          if (transaction) {
            // existing transaction has been confirmed
            transaction.set(transaction.parse(_.omit(e.jsonData.wallet, 'wallet')));
          } else {
            // new transaction
            this.newTransactionCount += 1;

            // This is a bit ugly... but most incoming transactions (ones sent via our UI)
            // are immediately added to the list when their respective APIs succeeds and
            // therefore should not be included in the "new transactions" pop in count.
            // But, at this point we don't know if these are such transactions, so we'll
            // check back in a bit and see if they've already been added or not. It's a matter
            // of the socket coming in before the AJAX call returns.
            const timeout = setTimeout(() => {
              if (this.collection.get(e.jsonData.wallet.txid)) {
                this.newTransactionCount -= 1;
              } else {
                this.showNewTransactionPopup();
              }
            }, 1500);

            this.popInTimeouts.push(timeout);
          }
        }

        // The "walletUpdate" socket comes on a regular interval and gives us the current block
        // height which we can use to update the confirmations on a transaction.
        if (e.jsonData.walletUpdate && e.jsonData.walletUpdate[this.coinType]) {
          const walletUpdate = e.jsonData.walletUpdate[this.coinType];
          this.collection.models
            .filter(transaction => (transaction.get('height') > 0))
            .forEach(transaction => {
              const confirmations =
                walletUpdate.height - transaction.get('height') + 1;
              transaction.set('confirmations', confirmations);
            });
        }
      });
    }

    this.$scrollContainer = opts.$scrollContainer;
    this.throttledOnScroll = _.throttle(this.onScroll, 100).bind(this);

    if (opts.fetchOnInit) this.fetchTransactions();
  }

  className() {
    return 'walletTransactions';
  }

  onScroll() {
    if (this.collection.length && !this.allLoaded) {
      // fetch next batch of transactions
      const lastTransaction = this.transactionViews[this.transactionViews.length - 1];

      if (!this.isFetching && isScrolledIntoView(lastTransaction.el)) {
        this.fetchTransactions();
      }
    }

    if (this.newTransactionPopIn) {
      this.setPopInMessageHolderPositioning();
    }
  }

  get transactionsPerFetch() {
    return 25;
  }

  get isFetching() {
    return this.transactionsFetch &&
      this.transactionsFetch.state() === 'pending';
  }

  get allLoaded() {
    return this.collection.length >= this.countAtFirstFetch;
  }

  fetchTransactions() {
    if (this.transactionsFetch &&
      this.transactionsFetch.state() === 'pending') return;

    const fetchParams = {
      limit: this.transactionsPerFetch,
    };

    if (this.collection.length) {
      fetchParams.offsetId = this.collection.at(this.collection.length - 1).id;
    }

    this.transactionsFetch = this.collection.fetch({
      data: fetchParams,
      remove: false,
    });

    this.transactionsFetch
      .done(data => {
        if (this.isRemoved()) return;

        this.fetchFailed = false;
        this.fetchErrorMessage = '';

        if (typeof this.countAtFirstFetch === 'undefined') {
          this.countAtFirstFetch = data.count;
        }

        if (this.collection.length) {
          this.transactionFetchState.setState({
            isFetching: false,
            fetchFailed: this.fetchFailed,
            fetchErrorMessage: this.fetchErrorMessage,
          });

          const curConn = getCurrentConnection();

          if (curConn && curConn.server && !curConn.server.get('backupWalletWarned')) {
            const warning = openSimpleMessage(
              app.polyglot.t('wallet.transactions.backupWalletWarningTitle'),
              app.polyglot.t('wallet.transactions.backupWalletWarningBody', {
                link: '<a class="js-recoverWalletSeed">' +
                  `${app.polyglot.t('wallet.transactions.recoverySeedLink')}</a>`,
              })
            );

            warning.$el.on('click', '.js-recoverWalletSeed', () => {
              launchSettingsModal({
                initialTab: 'Advanced',
                scrollTo: '.js-backupWalletSection',
              });
              warning.remove();
            });

            curConn.server.save({ backupWalletWarned: true });
          }
        } else {
          this.render();
        }
      }).fail(xhr => {
        if (this.isRemoved() || xhr.statusText === 'abort') return;

        this.fetchFailed = true;
        this.fetchErrorMessage = xhr.responseJSON && xhr.responseJSON.reason || '';

        if (this.collection.length) {
          this.transactionFetchState.setState({
            isFetching: false,
            fetchFailed: this.fetchFailed,
            fetchErrorMessage: this.fetchErrorMessage,
          });
        } else {
          this.render();
        }
      });

    if (this.transactionFetchState) {
      this.transactionFetchState.setState({
        isFetching: true,
        transactionsPresent: !!this.collection.length,
      });
    }

    this.fetchFailed = false;
    this.fetchErrorMessage = '';
  }

  setPopInMessageHolderPositioning() {
    this.$popInMessages.toggleClass('notFixed', this.$scrollContainer[0].scrollTop < 515);
  }

  showNewTransactionPopup() {
    if (this.newTransactionPopIn && !this.newTransactionPopIn.isRemoved()) {
      this.newTransactionPopIn.setState({
        messageText:
          buildRefreshAlertMessage(app.polyglot.t('wallet.transactions.newTransactionsPopin',
            { smart_count: this.newTransactionCount })),
      });
    } else {
      this.newTransactionPopIn = this.createChild(PopInMessage, {
        messageText:
          buildRefreshAlertMessage(app.polyglot.t('wallet.transactions.newTransactionsPopin',
            { smart_count: this.newTransactionCount })),
      });

      this.listenTo(this.newTransactionPopIn, 'clickRefresh', () => {
        this.collection.reset();
        this.fetchTransactions();
        this.render();
      });

      this.listenTo(this.newTransactionPopIn, 'clickDismiss', () => {
        this.newTransactionPopIn.remove();
        this.newTransactionPopIn = null;
      });

      this.$popInMessages.append(this.newTransactionPopIn.render().el);
      this.setPopInMessageHolderPositioning();
    }
  }

  createTransactionView(model, options = {}) {
    const view = this.createChild(Transaction, {
      model,
      coinType: this.coinType,
      bumpFeeXhr: this.options.bumpFeeXhrs &&
        this.options.bumpFeeXhrs[model.id] || undefined,
      ...options,
    });

    this.listenTo(view, 'bumpFeeSuccess', e =>
      this.trigger('bumpFeeSuccess', e));

    this.listenTo(view, 'bumpFeeAttempt', e =>
      this.trigger('bumpFeeAttempt', e));

    return view;
  }

  remove() {
    if (this.transactionsFetch) this.transactionsFetch.abort();
    this.popInTimeouts.forEach(timeout => clearTimeout(timeout));
    this.$scrollContainer.off('scroll', this.throttledOnScroll);
    super.remove();
  }

  get $popInMessages() {
    return this._$popInMessages ||
      (this._$popInMessages = this.$('.js-popInMessages'));
  }

  renderTransactions(models = [], insertionType = 'append') {
    if (!models) {
      throw new Error('Please provide an array of transactions models.');
    }

    if (['append', 'prepend', 'replace'].indexOf(insertionType) === -1) {
      throw new Error('Please provide a valid insertion type.');
    }

    if (insertionType === 'replace') {
      this.transactionViews.forEach(transaction => transaction.remove());
      this.transactionViews = [];
    }

    const transactionsFrag = document.createDocumentFragment();

    models.forEach(transaction => {
      const view = this.createTransactionView(transaction);
      this.transactionViews.push(view);
      view.render().$el.appendTo(transactionsFrag);
    });

    if (insertionType === 'prepend') {
      this.$transactionsContainer.prepend(transactionsFrag);
    } else {
      this.$transactionsContainer.append(transactionsFrag);
    }
  }

  render() {
    this.newTransactionCount = 0;
    this.popInTimeouts.forEach(timeout => clearTimeout(timeout));
    this.popInTimeouts = [];
    if (this.newTransactionPopIn) {
      this.newTransactionPopIn.remove();
      this.newTransactionPopIn = null;
    }

    loadTemplate('modals/wallet/transactions/transactions.html', (t) => {
      this.$el.html(t({
        transactions: this.collection.toJSON(),
        isFetching: this.isFetching,
        fetchFailed: this.fetchFailed,
        coinType: this.coinType,
      }));
    });

    this.$transactionsContainer = this.$('.js-transactionListContainer');
    this._$popInMessages = null;
    this._$noTransactions = null;

    this.renderTransactions(this.collection.models, 'replace');

    this.$scrollContainer.off('scroll', this.throttledOnScroll)
      .on('scroll', this.throttledOnScroll);

    if (this.transactionFetchState) this.transactionFetchState.remove();
    this.transactionFetchState = this.createChild(TransactionFetchState, {
      initialState: {
        isFetching: this.isFetching,
        fetchFailed: this.fetchFailed,
        fetchErrorMessage: this.fetchErrorMessage,
        transactionsPresent: !!this.collection.length,
      },
    });
    this.$('.js-transactionFetchStateWrap').html(this.transactionFetchState.render().el);
    this.listenTo(this.transactionFetchState, 'clickRetryFetch', () => {
      // simulate some latency so if it fails again, it looks like it tried.
      this.transactionFetchState.setState({ isFetching: true });
      setTimeout(() => this.fetchTransactions(), 250);
    });

    return this;
  }
}
<commit_msg>Only Count Unique Incoming TXs<commit_after>import _ from 'underscore';
import app from '../../../../app';
import { isScrolledIntoView } from '../../../../utils/dom';
import { getSocket, getCurrentConnection } from '../../../../utils/serverConnect';
import { openSimpleMessage } from '../../SimpleMessage';
import { launchSettingsModal } from '../../../../utils/modalManager';
import loadTemplate from '../../../../utils/loadTemplate';
import BaseVw from '../../../baseVw';
import Transaction from './Transaction';
import TransactionFetchState from './TransactionFetchState';
import PopInMessage, { buildRefreshAlertMessage } from '../../../components/PopInMessage';

export default class extends BaseVw {
  constructor(options = {}) {
    const opts = {
      fetchOnInit: true,
      // If not fetching on init, you may want to pass in the count of transactions
      // that were returned by the first fetch which is used to determine if all the
      // pages have been fetched.
      countAtFirstFetch: undefined,
      // If there are any existing bump fee attempts that you want shuttled into the
      // individual transaction views, please provide an indexed object (indexed by txid)
      // of them here.
      bumpFeeXhrs: undefined,
      ...options,
    };

    super(options);
    this.options = opts;

    if (!this.collection) {
      throw new Error('Please provide a Transactions collection.');
    }

    if (!opts.$scrollContainer || !opts.$scrollContainer.length) {
      throw new Error('Please provide a jQuery object containing the scrollable element ' +
        'this view is in.');
    }

    this.transactionViews = [];
    this.fetchFailed = false;
    this.fetchErrorMessage = '';
    this.newTransactionsTXs = new Set();
    this.popInTimeouts = [];
    this.coinType = this.collection.options.coinType;
    this.countAtFirstFetch = opts.countAtFirstFetch;

    this.listenTo(this.collection, 'update', (cl, clUpdateOpts) => {
      if (clUpdateOpts.changes.added.length) {
        // Expecting either a single new transactions on top or a page
        // of transactions on the bottom.
        if (clUpdateOpts.changes.added.length === this.collection.length ||
          clUpdateOpts.changes.added[clUpdateOpts.changes.added.length - 1] ===
            this.collection.at(this.collection.length - 1)) {
          // It's a page of transactions at the bottom
          this.renderTransactions(clUpdateOpts.changes.added, 'append');
        } else {
          // New transaction at top
          this.renderTransactions(clUpdateOpts.changes.added, 'prepend');
        }
      }
    });

    this.listenTo(app.settings, 'change:localCurrency', () => {
      this.renderTransactions(this.collection.models, 'replace');
    });

    this.listenTo(app.localSettings, 'change:bitcoinUnit', () => {
      this.renderTransactions(this.collection.models, 'replace');
    });

    const serverSocket = getSocket();

    if (serverSocket) {
      this.listenTo(serverSocket, 'message', e => {
        // "wallet" sockets come for new transactions and when a transaction gets it's
        // first confirmation.
        if (e.jsonData.wallet && e.jsonData.wallet.wallet === this.coinType) {
          const transaction = this.collection.get(e.jsonData.wallet.txid);

          if (transaction) {
            // existing transaction has been confirmed
            transaction.set(transaction.parse(_.omit(e.jsonData.wallet, 'wallet')));
          } else {
            // new transaction
            this.newTransactionsTXs.add(e.jsonData.wallet.txid);

            // This is a bit ugly... but most incoming transactions (ones sent via our UI)
            // are immediately added to the list when their respective APIs succeeds and
            // therefore should not be included in the "new transactions" pop in count.
            // But, at this point we don't know if these are such transactions, so we'll
            // check back in a bit and see if they've already been added or not. It's a matter
            // of the socket coming in before the AJAX call returns.
            const timeout = setTimeout(() => {
              if (this.collection.get(e.jsonData.wallet.txid)) {
                this.newTransactionsTXs.delete(e.jsonData.wallet.txid);
              } else {
                this.showNewTransactionPopup();
              }
            }, 1500);

            this.popInTimeouts.push(timeout);
          }
        }

        // The "walletUpdate" socket comes on a regular interval and gives us the current block
        // height which we can use to update the confirmations on a transaction.
        if (e.jsonData.walletUpdate && e.jsonData.walletUpdate[this.coinType]) {
          const walletUpdate = e.jsonData.walletUpdate[this.coinType];
          this.collection.models
            .filter(transaction => (transaction.get('height') > 0))
            .forEach(transaction => {
              const confirmations =
                walletUpdate.height - transaction.get('height') + 1;
              transaction.set('confirmations', confirmations);
            });
        }
      });
    }

    this.$scrollContainer = opts.$scrollContainer;
    this.throttledOnScroll = _.throttle(this.onScroll, 100).bind(this);

    if (opts.fetchOnInit) this.fetchTransactions();
  }

  className() {
    return 'walletTransactions';
  }

  onScroll() {
    if (this.collection.length && !this.allLoaded) {
      // fetch next batch of transactions
      const lastTransaction = this.transactionViews[this.transactionViews.length - 1];

      if (!this.isFetching && isScrolledIntoView(lastTransaction.el)) {
        this.fetchTransactions();
      }
    }

    if (this.newTransactionPopIn) {
      this.setPopInMessageHolderPositioning();
    }
  }

  get transactionsPerFetch() {
    return 25;
  }

  get isFetching() {
    return this.transactionsFetch &&
      this.transactionsFetch.state() === 'pending';
  }

  get allLoaded() {
    return this.collection.length >= this.countAtFirstFetch;
  }

  fetchTransactions() {
    if (this.transactionsFetch &&
      this.transactionsFetch.state() === 'pending') return;

    const fetchParams = {
      limit: this.transactionsPerFetch,
    };

    if (this.collection.length) {
      fetchParams.offsetId = this.collection.at(this.collection.length - 1).id;
    }

    this.transactionsFetch = this.collection.fetch({
      data: fetchParams,
      remove: false,
    });

    this.transactionsFetch
      .done(data => {
        if (this.isRemoved()) return;

        this.fetchFailed = false;
        this.fetchErrorMessage = '';

        if (typeof this.countAtFirstFetch === 'undefined') {
          this.countAtFirstFetch = data.count;
        }

        if (this.collection.length) {
          this.transactionFetchState.setState({
            isFetching: false,
            fetchFailed: this.fetchFailed,
            fetchErrorMessage: this.fetchErrorMessage,
          });

          const curConn = getCurrentConnection();

          if (curConn && curConn.server && !curConn.server.get('backupWalletWarned')) {
            const warning = openSimpleMessage(
              app.polyglot.t('wallet.transactions.backupWalletWarningTitle'),
              app.polyglot.t('wallet.transactions.backupWalletWarningBody', {
                link: '<a class="js-recoverWalletSeed">' +
                  `${app.polyglot.t('wallet.transactions.recoverySeedLink')}</a>`,
              })
            );

            warning.$el.on('click', '.js-recoverWalletSeed', () => {
              launchSettingsModal({
                initialTab: 'Advanced',
                scrollTo: '.js-backupWalletSection',
              });
              warning.remove();
            });

            curConn.server.save({ backupWalletWarned: true });
          }
        } else {
          this.render();
        }
      }).fail(xhr => {
        if (this.isRemoved() || xhr.statusText === 'abort') return;

        this.fetchFailed = true;
        this.fetchErrorMessage = xhr.responseJSON && xhr.responseJSON.reason || '';

        if (this.collection.length) {
          this.transactionFetchState.setState({
            isFetching: false,
            fetchFailed: this.fetchFailed,
            fetchErrorMessage: this.fetchErrorMessage,
          });
        } else {
          this.render();
        }
      });

    if (this.transactionFetchState) {
      this.transactionFetchState.setState({
        isFetching: true,
        transactionsPresent: !!this.collection.length,
      });
    }

    this.fetchFailed = false;
    this.fetchErrorMessage = '';
  }

  setPopInMessageHolderPositioning() {
    this.$popInMessages.toggleClass('notFixed', this.$scrollContainer[0].scrollTop < 515);
  }

  showNewTransactionPopup() {
    if (this.newTransactionPopIn && !this.newTransactionPopIn.isRemoved()) {
      this.newTransactionPopIn.setState({
        messageText:
          buildRefreshAlertMessage(app.polyglot.t('wallet.transactions.newTransactionsPopin',
            { smart_count: this.newTransactionsTXs.size })),
      });
    } else {
      this.newTransactionPopIn = this.createChild(PopInMessage, {
        messageText:
          buildRefreshAlertMessage(app.polyglot.t('wallet.transactions.newTransactionsPopin',
            { smart_count: this.newTransactionsTXs.size })),
      });

      this.listenTo(this.newTransactionPopIn, 'clickRefresh', () => {
        this.collection.reset();
        this.fetchTransactions();
        this.render();
      });

      this.listenTo(this.newTransactionPopIn, 'clickDismiss', () => {
        this.newTransactionPopIn.remove();
        this.newTransactionPopIn = null;
      });

      this.$popInMessages.append(this.newTransactionPopIn.render().el);
      this.setPopInMessageHolderPositioning();
    }
  }

  createTransactionView(model, options = {}) {
    const view = this.createChild(Transaction, {
      model,
      coinType: this.coinType,
      bumpFeeXhr: this.options.bumpFeeXhrs &&
        this.options.bumpFeeXhrs[model.id] || undefined,
      ...options,
    });

    this.listenTo(view, 'bumpFeeSuccess', e =>
      this.trigger('bumpFeeSuccess', e));

    this.listenTo(view, 'bumpFeeAttempt', e =>
      this.trigger('bumpFeeAttempt', e));

    return view;
  }

  remove() {
    if (this.transactionsFetch) this.transactionsFetch.abort();
    this.popInTimeouts.forEach(timeout => clearTimeout(timeout));
    this.$scrollContainer.off('scroll', this.throttledOnScroll);
    super.remove();
  }

  get $popInMessages() {
    return this._$popInMessages ||
      (this._$popInMessages = this.$('.js-popInMessages'));
  }

  renderTransactions(models = [], insertionType = 'append') {
    if (!models) {
      throw new Error('Please provide an array of transactions models.');
    }

    if (['append', 'prepend', 'replace'].indexOf(insertionType) === -1) {
      throw new Error('Please provide a valid insertion type.');
    }

    if (insertionType === 'replace') {
      this.transactionViews.forEach(transaction => transaction.remove());
      this.transactionViews = [];
    }

    const transactionsFrag = document.createDocumentFragment();

    models.forEach(transaction => {
      const view = this.createTransactionView(transaction);
      this.transactionViews.push(view);
      view.render().$el.appendTo(transactionsFrag);
    });

    if (insertionType === 'prepend') {
      this.$transactionsContainer.prepend(transactionsFrag);
    } else {
      this.$transactionsContainer.append(transactionsFrag);
    }
  }

  render() {
    this.newTransactionsTXs.clear();
    this.popInTimeouts.forEach(timeout => clearTimeout(timeout));
    this.popInTimeouts = [];
    if (this.newTransactionPopIn) {
      this.newTransactionPopIn.remove();
      this.newTransactionPopIn = null;
    }

    loadTemplate('modals/wallet/transactions/transactions.html', (t) => {
      this.$el.html(t({
        transactions: this.collection.toJSON(),
        isFetching: this.isFetching,
        fetchFailed: this.fetchFailed,
        coinType: this.coinType,
      }));
    });

    this.$transactionsContainer = this.$('.js-transactionListContainer');
    this._$popInMessages = null;
    this._$noTransactions = null;

    this.renderTransactions(this.collection.models, 'replace');

    this.$scrollContainer.off('scroll', this.throttledOnScroll)
      .on('scroll', this.throttledOnScroll);

    if (this.transactionFetchState) this.transactionFetchState.remove();
    this.transactionFetchState = this.createChild(TransactionFetchState, {
      initialState: {
        isFetching: this.isFetching,
        fetchFailed: this.fetchFailed,
        fetchErrorMessage: this.fetchErrorMessage,
        transactionsPresent: !!this.collection.length,
      },
    });
    this.$('.js-transactionFetchStateWrap').html(this.transactionFetchState.render().el);
    this.listenTo(this.transactionFetchState, 'clickRetryFetch', () => {
      // simulate some latency so if it fails again, it looks like it tried.
      this.transactionFetchState.setState({ isFetching: true });
      setTimeout(() => this.fetchTransactions(), 250);
    });

    return this;
  }
}
<|endoftext|>


<commit_before>window.User = Backbone.Model.extend({
  channels: [],
  setChannels: function(channels) {
    this.channels = channels;
  },

  url: function() {
    return '/' + this.get('username');
  }
});<commit_msg>Gave the user a new URL<commit_after>window.User = Backbone.Model.extend({
  channels: [],
  setChannels: function(channels) {
    this.channels = channels;
  },

  url: function() {
    return '/' + this.get('username') + "/channels/" + this.get('all_channel_id');
  }
});<|endoftext|>


<commit_before>import React from 'react';
import ReactTable from 'react-table';
import TextareaAutosize from 'react-autosize-textarea';
import alertify from 'alertifyjs';
import bem from 'js/bem';
import actions from 'js/actions';
import stores from 'js/stores';
import {MODAL_TYPES} from 'js/constants';
import {t} from 'utils';

const SAVE_BUTTON_TEXT = {
  DEFAULT: t('Save Changes'),
  UNSAVED: t('* Save Changes'),
  PENDING: t('Saving…')
};

export class TranslationTable extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      saveChangesButtonText: SAVE_BUTTON_TEXT.DEFAULT,
      isSaveChangesButtonPending: false,
      tableData: []
    };
    stores.translations.setTranslationTableUnsaved(false);
    const {translated, survey, choices, translations} = props.asset.content;
    const langIndex = props.langIndex;

    // add each translatable property for survey items to translation table
    survey.forEach((row) => {
      translated.forEach((property) => {
        if (row[property] && row[property][0]) {
          this.state.tableData.push({
            original: row[property][0],
            value: row[property][langIndex],
            name: row.name || row.$autoname,
            itemProp: property,
            contentProp: 'survey'
          });
        }
      });
    });

    // add choice options to translation table
    if (choices && choices.length) {
      choices.forEach((choice) => {
        if (choice.label && choice.label[0]) {
          this.state.tableData.push({
            original: choice.label[0],
            value: choice.label[langIndex],
            name: choice.name || choice.$autovalue,
            itemProp: 'label',
            contentProp: 'choices'
          });
        }
      });
    }

    this.columns = [
      {
        Header: t('Original string'),
        accessor: 'original',
        minWidth: 130,
        Cell: (cellInfo) => {return cellInfo.original.original;}
      }, {
        Header: t('##lang## translation').replace('##lang##', translations[langIndex]),
        accessor: 'translation',
        className: 'translation',
        Cell: (cellInfo) => {
          return (
            <TextareaAutosize
              onChange={(e) => {
                const data = [...this.state.tableData];
                data[cellInfo.index].value = e.target.value;
                this.setState({ data });
                this.markFormUnsaved();
              }}
              value={this.state.tableData[cellInfo.index].value || ''}
            />
          );
        }
      }
    ];
  }

  markFormUnsaved() {
    this.setState({
      saveChangesButtonText: SAVE_BUTTON_TEXT.UNSAVED,
      isSaveChangesButtonPending: false
    });
    stores.translations.setTranslationTableUnsaved(true);
  }

  markFormPending() {
    this.setState({
      saveChangesButtonText: SAVE_BUTTON_TEXT.PENDING,
      isSaveChangesButtonPending: true
    });
    stores.translations.setTranslationTableUnsaved(true);
  }

  markFormIdle() {
    this.setState({
      saveChangesButtonText: SAVE_BUTTON_TEXT.DEFAULT,
      isSaveChangesButtonPending: false
    });
    stores.translations.setTranslationTableUnsaved(false);
  }

  saveChanges() {
    let content = this.props.asset.content,
        rows = this.state.tableData,
        langIndex = this.props.langIndex;
    for (var i = 0, len = rows.length; i < len; i++) {
      let contentProp = rows[i].contentProp,
          item = content[rows[i].contentProp].find(o => o.name === rows[i].name || o.$autoname === rows[i].name || o.$autovalue === rows[i].name),
          itemProp = rows[i].itemProp;

      if (item[itemProp][langIndex] !== rows[i].value) {
        item[itemProp][langIndex] = rows[i].value;
      }
    }

    this.markFormPending();
    actions.resources.updateAsset(
      this.props.asset.uid,
      {
        content: JSON.stringify(content)
      },
      {
        onComplete: this.markFormIdle.bind(this),
        onFailed: this.markFormUnsaved.bind(this)
      }
    );
  }

  onBack() {
    if (stores.translations.state.isTranslationTableUnsaved) {
      const dialog = alertify.dialog('confirm');
      const opts = {
        title: t('Go back?'),
        message: t('You will lose all unsaved changes.'),
        labels: {ok: t('Confirm'), cancel: t('Cancel')},
        onok: this.showManageLanguagesModal.bind(this),
        oncancel: dialog.destroy
      };
      dialog.set(opts).show();
    } else {
      this.showManageLanguagesModal();
    }
  }

  showManageLanguagesModal() {
    stores.pageState.switchModal({
      type: MODAL_TYPES.FORM_LANGUAGES,
      asset: this.props.asset
    });
  }

  render () {
    return (
      <bem.FormModal m='translation-table'>

          <ReactTable
            data={this.state.tableData}
            columns={this.columns}
            defaultPageSize={30}
            showPageSizeOptions={false}
            previousText={t('Prev')}
            nextText={t('Next')}
            minRows={1}
            loadingText={

                {t('Loading...')}

            }
          />

        <bem.Modal__footer>
          <bem.Modal__footerButton
            m='back'
            onClick={this.onBack.bind(this)}
          >
            {t('Back')}
          </bem.Modal__footerButton>

          <bem.Modal__footerButton
            m='primary'
            onClick={this.saveChanges.bind(this)}
            disabled={this.state.isSaveChangesButtonPending}
          >
            {this.state.saveChangesButtonText}
          </bem.Modal__footerButton>
        </bem.Modal__footer>
      </bem.FormModal>
    );
  }
}

export default TranslationTable;
<commit_msg>fixes kobotoolbox/kpi/issues/2274 - different lists choices translating bug<commit_after>import React from 'react';
import ReactTable from 'react-table';
import TextareaAutosize from 'react-autosize-textarea';
import alertify from 'alertifyjs';
import bem from 'js/bem';
import actions from 'js/actions';
import stores from 'js/stores';
import {MODAL_TYPES} from 'js/constants';
import {t} from 'utils';

const SAVE_BUTTON_TEXT = {
  DEFAULT: t('Save Changes'),
  UNSAVED: t('* Save Changes'),
  PENDING: t('Saving…')
};

export class TranslationTable extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      saveChangesButtonText: SAVE_BUTTON_TEXT.DEFAULT,
      isSaveChangesButtonPending: false,
      tableData: []
    };
    stores.translations.setTranslationTableUnsaved(false);
    const {translated, survey, choices, translations} = props.asset.content;
    const langIndex = props.langIndex;

    // add each translatable property for survey items to translation table
    survey.forEach((row) => {
      translated.forEach((property) => {
        if (row[property] && row[property][0]) {
          this.state.tableData.push({
            original: row[property][0],
            value: row[property][langIndex],
            name: row.name || row.$autoname,
            itemProp: property,
            contentProp: 'survey'
          });
        }
      });
    });

    // add choice options to translation table
    if (choices && choices.length) {
      choices.forEach((choice) => {
        if (choice.label && choice.label[0]) {
          this.state.tableData.push({
            original: choice.label[0],
            value: choice.label[langIndex],
            name: choice.name || choice.$autovalue,
            listName: choice.list_name,
            itemProp: 'label',
            contentProp: 'choices'
          });
        }
      });
    }

    this.columns = [
      {
        Header: t('Original string'),
        accessor: 'original',
        minWidth: 130,
        Cell: (cellInfo) => {return cellInfo.original.original;}
      }, {
        Header: t('##lang## translation').replace('##lang##', translations[langIndex]),
        accessor: 'translation',
        className: 'translation',
        Cell: (cellInfo) => {
          return (
            <TextareaAutosize
              onChange={(e) => {
                const data = [...this.state.tableData];
                data[cellInfo.index].value = e.target.value;
                this.setState({ data });
                this.markFormUnsaved();
              }}
              value={this.state.tableData[cellInfo.index].value || ''}
            />
          );
        }
      }
    ];
  }

  markFormUnsaved() {
    this.setState({
      saveChangesButtonText: SAVE_BUTTON_TEXT.UNSAVED,
      isSaveChangesButtonPending: false
    });
    stores.translations.setTranslationTableUnsaved(true);
  }

  markFormPending() {
    this.setState({
      saveChangesButtonText: SAVE_BUTTON_TEXT.PENDING,
      isSaveChangesButtonPending: true
    });
    stores.translations.setTranslationTableUnsaved(true);
  }

  markFormIdle() {
    this.setState({
      saveChangesButtonText: SAVE_BUTTON_TEXT.DEFAULT,
      isSaveChangesButtonPending: false
    });
    stores.translations.setTranslationTableUnsaved(false);
  }

  saveChanges() {
    let content = this.props.asset.content,
        rows = this.state.tableData,
        langIndex = this.props.langIndex;
    for (var i = 0, len = rows.length; i < len; i++) {
      let item = content[rows[i].contentProp].find((o) => {
        return (
          o.name === rows[i].name ||
          o.$autoname === rows[i].name ||
          o.$autovalue === rows[i].name
        ) && o.list_name === rows[i].listName;
      });
      let itemProp = rows[i].itemProp;

      if (item[itemProp][langIndex] !== rows[i].value) {
        item[itemProp][langIndex] = rows[i].value;
      }
    }

    this.markFormPending();
    actions.resources.updateAsset(
      this.props.asset.uid,
      {
        content: JSON.stringify(content)
      },
      {
        onComplete: this.markFormIdle.bind(this),
        onFailed: this.markFormUnsaved.bind(this)
      }
    );
  }

  onBack() {
    if (stores.translations.state.isTranslationTableUnsaved) {
      const dialog = alertify.dialog('confirm');
      const opts = {
        title: t('Go back?'),
        message: t('You will lose all unsaved changes.'),
        labels: {ok: t('Confirm'), cancel: t('Cancel')},
        onok: this.showManageLanguagesModal.bind(this),
        oncancel: dialog.destroy
      };
      dialog.set(opts).show();
    } else {
      this.showManageLanguagesModal();
    }
  }

  showManageLanguagesModal() {
    stores.pageState.switchModal({
      type: MODAL_TYPES.FORM_LANGUAGES,
      asset: this.props.asset
    });
  }

  render () {
    return (
      <bem.FormModal m='translation-table'>

          <ReactTable
            data={this.state.tableData}
            columns={this.columns}
            defaultPageSize={30}
            showPageSizeOptions={false}
            previousText={t('Prev')}
            nextText={t('Next')}
            minRows={1}
            loadingText={

                {t('Loading...')}

            }
          />

        <bem.Modal__footer>
          <bem.Modal__footerButton
            m='back'
            onClick={this.onBack.bind(this)}
          >
            {t('Back')}
          </bem.Modal__footerButton>

          <bem.Modal__footerButton
            m='primary'
            onClick={this.saveChanges.bind(this)}
            disabled={this.state.isSaveChangesButtonPending}
          >
            {this.state.saveChangesButtonText}
          </bem.Modal__footerButton>
        </bem.Modal__footer>
      </bem.FormModal>
    );
  }
}

export default TranslationTable;
<|endoftext|>


<commit_before>const Util = require('../util');
const Animate = require('./animate');
const { MatrixUtil } = require('@ali/g');
const { mat3 } = MatrixUtil;

// 获取图组内所有的shapes
function getShapes(container, viewId) {
  let shapes = [];
  if (container.get('animate') === false) {
    return [];
  }
  const children = container.get('children');
  Util.each(children, child => {
    if (child.isGroup) {
      shapes = shapes.concat(getShapes(child, viewId));
    } else if (child.isShape && child._id) {
      let id = child._id;
      id = id.split('-')[0];
      if (id === viewId) {
        shapes.push(child);
      }
    }
  });

  return shapes;
}

function cache(shapes) {
  const rst = {};
  Util.each(shapes, shape => {
    if (!shape._id || shape.isClip) return;
    const id = shape._id;
    rst[id] = {
      _id: id,
      type: shape.get('type'),
      attrs: Util.cloneDeep(shape.__attrs), // 原始属性
      name: shape.name,
      index: shape.get('index'),
      animateCfg: shape.get('animateCfg'),
      coord: shape.get('coord')
    };
  });
  return rst;
}

function getAnimate(geomType, coord, animationType, animationName) {
  let result;
  if (animationName) {
    result = Animate.Action[animationType][animationName];
  } else {
    result = Animate.getAnimation(geomType, coord, animationType);
  }
  return result;
}

function getAnimateCfg(geomType, animationType, animateCfg) {
  const defaultCfg = Animate.getAnimateCfg(geomType, animationType);
  if (animateCfg && animateCfg[animationType]) {
    return Util.defaultsDeep(animateCfg[animationType], defaultCfg);
  }
  return defaultCfg;
}

function addAnimate(cache, shapes, canvas, isUpdate) {
  let animate;
  let animateCfg;

  if (isUpdate) {
    // Step: leave -> update -> enter
    const updateShapes = []; // 存储的是 shapes
    const newShapes = []; // 存储的是 shapes
    Util.each(shapes, shape => {
      const result = cache[shape._id];
      if (!result) {
        newShapes.push(shape);
      } else {
        shape.setSilent('cacheShape', result);
        updateShapes.push(shape);
        delete cache[shape._id];
      }
    });

    Util.each(cache, deletedShape => {
      const { name, coord, _id, attrs, index, type } = deletedShape;
      animateCfg = getAnimateCfg(name, 'leave', deletedShape.animateCfg);
      animate = getAnimate(name, coord, 'leave', animateCfg.animation);
      if (Util.isFunction(animate)) {
        const tempShape = canvas.addShape(type, {
          attrs,
          index
        });
        tempShape._id = _id;
        tempShape.name = name;
        if (coord) {
          const tempShapeMatrix = tempShape.getMatrix();
          const finalMatrix = mat3.multiply([], tempShapeMatrix, coord.matrix);
          tempShape.setMatrix(finalMatrix);
        }
        animate(tempShape, animateCfg, coord);
      }
    });

    Util.each(updateShapes, updateShape => {
      const name = updateShape.name;
      const coord = updateShape.get('coord');
      const cacheAttrs = updateShape.get('cacheShape').attrs;
      // 判断如果属性相同的话就不进行变换
      if (!Util.isEqual(cacheAttrs, updateShape.__attrs)) {
        animateCfg = getAnimateCfg(name, 'update', updateShape.get('animateCfg'));
        animate = getAnimate(name, coord, 'update', animateCfg.animation);
        if (Util.isFunction(animate)) {
          animate(updateShape, animateCfg, coord);
        } else {
          const endState = Util.cloneDeep(updateShape.__attrs);
          // updateShape.__attrs = cacheAttrs;
          updateShape.attr(cacheAttrs);
          updateShape.animate(endState, animateCfg.duration, animateCfg.easing, function() {
            updateShape.setSilent('cacheShape', null);
          });
        }
      }
    });

    Util.each(newShapes, newShape => {
      const name = newShape.name;
      const coord = newShape.get('coord');

      animateCfg = getAnimateCfg(name, 'enter', newShape.get('animateCfg'));
      animate = getAnimate(name, coord, 'enter', animateCfg.animation);
      if (Util.isFunction(animate)) {
        animate(newShape, animateCfg, coord);
      }
    });
  } else {
    Util.each(shapes, shape => {
      const name = shape.name;
      const coord = shape.get('coord');
      animateCfg = getAnimateCfg(name, 'appear', shape.get('animateCfg'));
      animate = getAnimate(name, coord, 'appear', animateCfg.animation);
      if (Util.isFunction(animate)) {
        animate(shape, animateCfg, coord);
      }
    });
  }
}

module.exports = {
  execAnimation(view, isUpdate) {
    const viewContainer = view.get('middlePlot');
    const axisContainer = view.get('backPlot');
    const viewId = view.get('_id');
    const canvas = view.get('canvas');
    const caches = canvas.get(viewId + 'caches') || [];
    const shapes = getShapes(viewContainer, viewId);
    const axisShapes = getShapes(axisContainer, viewId);
    const cacheShapes = shapes.concat(axisShapes);
    canvas.setSilent(viewId + 'caches', cache(cacheShapes));
    if (isUpdate) {
      addAnimate(caches, cacheShapes, canvas, isUpdate);
    } else {
      addAnimate(caches, shapes, canvas, isUpdate);
    }
  }
};
<commit_msg>保证动画状态下 canvas 被绘制。<commit_after>const Util = require('../util');
const Animate = require('./animate');
const { MatrixUtil } = require('@ali/g');
const { mat3 } = MatrixUtil;

// 获取图组内所有的shapes
function getShapes(container, viewId) {
  let shapes = [];
  if (container.get('animate') === false) {
    return [];
  }
  const children = container.get('children');
  Util.each(children, child => {
    if (child.isGroup) {
      shapes = shapes.concat(getShapes(child, viewId));
    } else if (child.isShape && child._id) {
      let id = child._id;
      id = id.split('-')[0];
      if (id === viewId) {
        shapes.push(child);
      }
    }
  });

  return shapes;
}

function cache(shapes) {
  const rst = {};
  Util.each(shapes, shape => {
    if (!shape._id || shape.isClip) return;
    const id = shape._id;
    rst[id] = {
      _id: id,
      type: shape.get('type'),
      attrs: Util.cloneDeep(shape.__attrs), // 原始属性
      name: shape.name,
      index: shape.get('index'),
      animateCfg: shape.get('animateCfg'),
      coord: shape.get('coord')
    };
  });
  return rst;
}

function getAnimate(geomType, coord, animationType, animationName) {
  let result;
  if (animationName) {
    result = Animate.Action[animationType][animationName];
  } else {
    result = Animate.getAnimation(geomType, coord, animationType);
  }
  return result;
}

function getAnimateCfg(geomType, animationType, animateCfg) {
  const defaultCfg = Animate.getAnimateCfg(geomType, animationType);
  if (animateCfg && animateCfg[animationType]) {
    return Util.defaultsDeep(animateCfg[animationType], defaultCfg);
  }
  return defaultCfg;
}

function addAnimate(cache, shapes, canvas, isUpdate) {
  let animate;
  let animateCfg;
  let canvasDrawn = false;

  if (isUpdate) {
    // Step: leave -> update -> enter
    const updateShapes = []; // 存储的是 shapes
    const newShapes = []; // 存储的是 shapes
    Util.each(shapes, shape => {
      const result = cache[shape._id];
      if (!result) {
        newShapes.push(shape);
      } else {
        shape.setSilent('cacheShape', result);
        updateShapes.push(shape);
        delete cache[shape._id];
      }
    });

    Util.each(cache, deletedShape => {
      const { name, coord, _id, attrs, index, type } = deletedShape;
      animateCfg = getAnimateCfg(name, 'leave', deletedShape.animateCfg);
      animate = getAnimate(name, coord, 'leave', animateCfg.animation);
      if (Util.isFunction(animate)) {
        const tempShape = canvas.addShape(type, {
          attrs,
          index
        });
        tempShape._id = _id;
        tempShape.name = name;
        if (coord) {
          const tempShapeMatrix = tempShape.getMatrix();
          const finalMatrix = mat3.multiply([], tempShapeMatrix, coord.matrix);
          tempShape.setMatrix(finalMatrix);
        }
        canvasDrawn = true;
        animate(tempShape, animateCfg, coord);
      }
    });

    Util.each(updateShapes, updateShape => {
      const name = updateShape.name;
      const coord = updateShape.get('coord');
      const cacheAttrs = updateShape.get('cacheShape').attrs;
      // 判断如果属性相同的话就不进行变换
      if (!Util.isEqual(cacheAttrs, updateShape.__attrs)) {
        animateCfg = getAnimateCfg(name, 'update', updateShape.get('animateCfg'));
        animate = getAnimate(name, coord, 'update', animateCfg.animation);
        if (Util.isFunction(animate)) {
          animate(updateShape, animateCfg, coord);
        } else {
          const endState = Util.cloneDeep(updateShape.__attrs);
          // updateShape.__attrs = cacheAttrs;
          updateShape.attr(cacheAttrs);
          updateShape.animate(endState, animateCfg.duration, animateCfg.easing, function() {
            updateShape.setSilent('cacheShape', null);
          });
        }
        canvasDrawn = true;
      }
    });

    Util.each(newShapes, newShape => {
      const name = newShape.name;
      const coord = newShape.get('coord');

      animateCfg = getAnimateCfg(name, 'enter', newShape.get('animateCfg'));
      animate = getAnimate(name, coord, 'enter', animateCfg.animation);
      if (Util.isFunction(animate)) {
        animate(newShape, animateCfg, coord);
        canvasDrawn = true;
      }
    });
  } else {
    Util.each(shapes, shape => {
      const name = shape.name;
      const coord = shape.get('coord');
      animateCfg = getAnimateCfg(name, 'appear', shape.get('animateCfg'));
      animate = getAnimate(name, coord, 'appear', animateCfg.animation);
      if (Util.isFunction(animate)) {
        animate(shape, animateCfg, coord);
        canvasDrawn = true;
      }
    });
  }
  return canvasDrawn;
}

module.exports = {
  execAnimation(view, isUpdate) {
    const viewContainer = view.get('middlePlot');
    const axisContainer = view.get('backPlot');
    const viewId = view.get('_id');
    const canvas = view.get('canvas');
    const caches = canvas.get(viewId + 'caches') || [];
    const shapes = getShapes(viewContainer, viewId);
    const axisShapes = getShapes(axisContainer, viewId);
    const cacheShapes = shapes.concat(axisShapes);
    canvas.setSilent(viewId + 'caches', cache(cacheShapes));
    let drawn;
    if (isUpdate) {
      drawn = addAnimate(caches, cacheShapes, canvas, isUpdate);
    } else {
      drawn = addAnimate(caches, shapes, canvas, isUpdate);
    }
    if (!drawn) {
      canvas.draw();
    }
  }
};
<|endoftext|>


<commit_before>import shallowEqual from 'client/utils/shallow-equal-except'

// will ignore router-specific props that usually cause unwanted re-render when modal is opened and custom
// route is used for modal aka "pinterest" or "product hunt" style modals
const pureComponentWithRoutedModal = component => {
  // can't be an arrow finction since need bounded thist to component's instance
  component.prototype.shouldComponentUpdate = function(nextProps, nextState) {
    if (nextProps.location && nextProps.location.state
      && (nextProps.location.state.modal || nextProps.location.state.navigatedBackFromModal)
    ) {
      return !shallowEqual(this.props, nextProps, {exceptProps: ['location', 'params', 'routes']})
    }

    return !shallowEqual(this.props, nextProps)
  }

  return component
}

export default pureComponentWithRoutedModal
<commit_msg>re-formatted params<commit_after>import shallowEqual from 'client/utils/shallow-equal-except'

// will ignore router-specific props that usually cause unwanted re-render when modal is opened and custom
// route is used for modal aka "pinterest" or "product hunt" style modals
const pureComponentWithRoutedModal = component => {
  // can't be an arrow finction since need bounded thist to component's instance
  component.prototype.shouldComponentUpdate = function(nextProps, nextState) {
    if (nextProps.location && nextProps.location.state
      && (nextProps.location.state.modal || nextProps.location.state.navigatedBackFromModal)
    ) {
      return !shallowEqual(this.props, nextProps, {
        exceptProps: ['location', 'params', 'routes']
      })
    }

    return !shallowEqual(this.props, nextProps)
  }

  return component
}

export default pureComponentWithRoutedModal
<|endoftext|>


<commit_before>/**
 * @file LazyImage component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import classNames from 'classnames';

import CircularLoading from '../CircularLoading';

import Event from '../_vendors/Event';
import debounce from 'lodash/debounce';

class LazyImage extends Component {

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            imageState: 0
        };

        this.scrollHandler = ::this.scrollHandler;

    }

    scrollHandler = debounce(() => {

        if (this.state.imageState > 0 || !this.wrapperEl) {
            return;
        }

        if (this.wrapperEl.getBoundingClientRect().top < window.innerHeight) {

            const {onImageLoadStart} = this.props;
            let result;

            if (onImageLoadStart) {
                result = onImageLoadStart();
            }

            if (result === false) {
                return;
            }

            this.setState({
                imageState: 1
            }, () => {

                const image = new Image();

                Event.addEvent(image, 'load', e => {
                    this.setState({
                        imageState: 2
                    }, () => {
                        const {onImageLoaded} = this.props;
                        onImageLoaded && onImageLoaded(e);
                    });
                });

                image.src = this.props.src;

            });

        }

    }, 250);

    componentDidMount() {
        this.wrapperEl = this.refs.wrapper;
        Event.addEvent(document, 'scroll', this.scrollHandler);
    }

    componentWillUnmount() {
        Event.removeEvent(document, 'scroll', this.scrollHandler);
    }

    render() {

        const {className, style, src, alt, loadingWidth, loadingHeight, width, height, placeholder} = this.props,
            {imageState} = this.state,

            lazyImageClassName = classNames('lazy-image', {
                [className]: className
            }),
            lazyImageStyle = {
                ...style,
                width: imageState === 2 ? width : loadingWidth,
                height: imageState === 2 ? height : loadingHeight
            },

            placeholderClassName = classNames('lazy-image-placeholder', {
                hidden: imageState === 2
            });

        return (

                    {
                        placeholder ?
                            {placeholder}
                            :
                            <CircularLoading className="lazy-image-loading"/>
                    }

        );

    }
};

LazyImage.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * Image src url.
     */
    src: PropTypes.string.isRequired,

    /**
     * Image alt text.
     */
    alt: PropTypes.string,

    /**
     * Loading width.
     */
    loadingWidth: PropTypes.number,

    /**
     * Loading height.
     */
    loadingHeight: PropTypes.number,

    /**
     * Image width.
     */
    width: PropTypes.number,

    /**
     * Image height.
     */
    height: PropTypes.number,

    /**
     * Image placeholder.
     */
    placeholder: PropTypes.any,

    /**
     * Image load start callback.
     */
    onImageLoadStart: PropTypes.func,

    /**
     * Image load end callback.
     */
    onImageLoaded: PropTypes.func

};

LazyImage.defaultProps = {

    className: '',
    style: null,

    alt: '',

    loadingWidth: 100,
    loadingHeight: 100,

    placeholder: ''

};

export default LazyImage;<commit_msg>update LazyImage<commit_after>/**
 * @file LazyImage component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import classNames from 'classnames';

import CircularLoading from '../CircularLoading';

import Event from '../_vendors/Event';

class LazyImage extends Component {

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            imageState: 0
        };

        this.scrollHandler = ::this.scrollHandler;

    }

    scrollHandler = debounce(() => {

        if (this.state.imageState > 0 || !this.wrapperEl) {
            return;
        }

        if (this.wrapperEl.getBoundingClientRect().top < window.innerHeight) {

            const {onImageLoadStart} = this.props;
            let result;

            if (onImageLoadStart) {
                result = onImageLoadStart();
            }

            if (result === false) {
                return;
            }

            this.setState({
                imageState: 1
            }, () => {

                const image = new Image();

                Event.addEvent(image, 'load', e => {
                    this.setState({
                        imageState: 2
                    }, () => {
                        const {onImageLoaded} = this.props;
                        onImageLoaded && onImageLoaded(e);
                    });
                });

                image.src = this.props.src;

            });

        }

    }, 250);

    componentDidMount() {
        this.wrapperEl = this.refs.wrapper;
        Event.addEvent(document, 'scroll', this.scrollHandler);
    }

    componentWillUnmount() {
        Event.removeEvent(document, 'scroll', this.scrollHandler);
    }

    render() {

        const {className, style, src, alt, loadingWidth, loadingHeight, width, height, placeholder} = this.props,
            {imageState} = this.state,

            lazyImageClassName = classNames('lazy-image', {
                [className]: className
            }),
            lazyImageStyle = {
                ...style,
                width: imageState === 2 ? width : loadingWidth,
                height: imageState === 2 ? height : loadingHeight
            },

            placeholderClassName = classNames('lazy-image-placeholder', {
                hidden: imageState === 2
            });

        return (

                    {
                        placeholder ?
                            {placeholder}
                            :
                            <CircularLoading className="lazy-image-loading"/>
                    }

        );

    }
};

LazyImage.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * Image src url.
     */
    src: PropTypes.string.isRequired,

    /**
     * Image alt text.
     */
    alt: PropTypes.string,

    /**
     * Loading width.
     */
    loadingWidth: PropTypes.number,

    /**
     * Loading height.
     */
    loadingHeight: PropTypes.number,

    /**
     * Image width.
     */
    width: PropTypes.number,

    /**
     * Image height.
     */
    height: PropTypes.number,

    /**
     * Image placeholder.
     */
    placeholder: PropTypes.any,

    /**
     * Image load start callback.
     */
    onImageLoadStart: PropTypes.func,

    /**
     * Image load end callback.
     */
    onImageLoaded: PropTypes.func

};

LazyImage.defaultProps = {

    className: '',
    style: null,

    alt: '',

    loadingWidth: 100,
    loadingHeight: 100,

    placeholder: ''

};

export default LazyImage;<|endoftext|>


<commit_before>// Process *this* and _that_
//
'use strict';

var isWhiteSpace   = require('../common/utils').isWhiteSpace;
var isPunctChar    = require('../common/utils').isPunctChar;
var isMdAsciiPunct = require('../common/utils').isMdAsciiPunct;

function isAlphaNum(code) {
  return (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */) ||
         (code >= 0x41 /* A */ && code <= 0x5A /* Z */) ||
         (code >= 0x61 /* a */ && code <= 0x7A /* z */);
}

// parse sequence of emphasis markers,
// "start" should point at a valid marker
function scanDelims(state, start) {
  var pos = start, lastChar, nextChar, count,
      isLastWhiteSpace, isLastPunctChar,
      isNextWhiteSpace, isNextPunctChar,
      can_open = true,
      can_close = true,
      max = state.posMax,
      marker = state.src.charCodeAt(start);

  lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1;

  while (pos < max && state.src.charCodeAt(pos) === marker) { pos++; }
  if (pos >= max) { can_open = false; }
  count = pos - start;

  nextChar = pos < max ? state.src.charCodeAt(pos) : -1;

  isLastPunctChar = lastChar >= 0 &&
    (isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)));
  isNextPunctChar = nextChar >= 0 &&
    (isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)));
  isLastWhiteSpace = lastChar >= 0 && isWhiteSpace(lastChar);
  isNextWhiteSpace = nextChar >= 0 && isWhiteSpace(nextChar);

  if (isNextWhiteSpace) {
    can_open = false;
  } else if (isNextPunctChar) {
    if (!(isLastWhiteSpace || isLastPunctChar || lastChar === -1)) {
      can_open = false;
    }
  }

  if (isLastWhiteSpace) {
    can_close = false;
  } else if (isLastPunctChar) {
    if (!(isNextWhiteSpace || isNextPunctChar || nextChar === -1)) {
      can_close = false;
    }
  }

  if (marker === 0x5F /* _ */) {
    // check if we aren't inside the word
    if (isAlphaNum(lastChar)) { can_open = false; }
    if (isAlphaNum(nextChar)) { can_close = false; }
  }

  return {
    can_open: can_open,
    can_close: can_close,
    delims: count
  };
}

module.exports = function emphasis(state, silent) {
  var startCount,
      count,
      found,
      oldCount,
      newCount,
      stack,
      res,
      max = state.posMax,
      start = state.pos,
      marker = state.src.charCodeAt(start);

  if (marker !== 0x5F/* _ */ && marker !== 0x2A /* * */) { return false; }
  if (silent) { return false; } // don't run any pairs in validation mode

  res = scanDelims(state, start);
  startCount = res.delims;
  if (!res.can_open) {
    state.pos += startCount;
    // Earlier we checked !silent, but this implementation does not need it
    state.pending += state.src.slice(start, state.pos);
    return true;
  }

  state.pos = start + startCount;
  stack = [ startCount ];

  while (state.pos < max) {
    if (state.src.charCodeAt(state.pos) === marker) {
      res = scanDelims(state, state.pos);
      count = res.delims;
      if (res.can_close) {
        oldCount = stack.pop();
        newCount = count;

        while (oldCount !== newCount) {
          if (newCount < oldCount) {
            stack.push(oldCount - newCount);
            break;
          }

          // assert(newCount > oldCount)
          newCount -= oldCount;

          if (stack.length === 0) { break; }
          state.pos += oldCount;
          oldCount = stack.pop();
        }

        if (stack.length === 0) {
          startCount = oldCount;
          found = true;
          break;
        }
        state.pos += count;
        continue;
      }

      if (res.can_open) { stack.push(count); }
      state.pos += count;
      continue;
    }

    state.md.inline.skipToken(state);
  }

  if (!found) {
    // parser failed to find ending tag, so it's not valid emphasis
    state.pos = start;
    return false;
  }

  // found!
  state.posMax = state.pos;
  state.pos = start + startCount;

  // Earlier we checked !silent, but this implementation does not need it

  // we have `startCount` starting and ending markers,
  // now trying to serialize them into tokens
  for (count = startCount; count > 1; count -= 2) {
    state.push({ type: 'strong_open', level: state.level++ });
  }
  if (count % 2) { state.push({ type: 'em_open', level: state.level++ }); }

  state.md.inline.tokenize(state);

  if (count % 2) { state.push({ type: 'em_close', level: --state.level }); }
  for (count = startCount; count > 1; count -= 2) {
    state.push({ type: 'strong_close', level: --state.level });
  }

  state.pos = state.posMax + startCount;
  state.posMax = max;
  return true;
};
<commit_msg>Bring emphasis up to spec<commit_after>// Process *this* and _that_
//
'use strict';

var isWhiteSpace   = require('../common/utils').isWhiteSpace;
var isPunctChar    = require('../common/utils').isPunctChar;
var isMdAsciiPunct = require('../common/utils').isMdAsciiPunct;

// parse sequence of emphasis markers,
// "start" should point at a valid marker
function scanDelims(state, start) {
  var pos = start, lastChar, nextChar, count,
      isLastWhiteSpace, isLastPunctChar,
      isNextWhiteSpace, isNextPunctChar,
      can_open = true,
      can_close = true,
      max = state.posMax,
      marker = state.src.charCodeAt(start);

  lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1;

  while (pos < max && state.src.charCodeAt(pos) === marker) { pos++; }
  if (pos >= max) { can_open = false; }
  count = pos - start;

  nextChar = pos < max ? state.src.charCodeAt(pos) : -1;

  isLastPunctChar = lastChar >= 0 &&
    (isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)));
  isNextPunctChar = nextChar >= 0 &&
    (isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)));

  // begin/end of the line counts as a whitespace too
  isLastWhiteSpace = lastChar < 0 || isWhiteSpace(lastChar);
  isNextWhiteSpace = nextChar < 0 || isWhiteSpace(nextChar);

  if (isNextWhiteSpace) {
    can_open = false;
  } else if (isNextPunctChar) {
    if (!(isLastWhiteSpace || isLastPunctChar)) {
      can_open = false;
    }
  }

  if (isLastWhiteSpace) {
    can_close = false;
  } else if (isLastPunctChar) {
    if (!(isNextWhiteSpace || isNextPunctChar)) {
      can_close = false;
    }
  }

  if (marker === 0x5F /* _ */) {
    if (can_open && can_close) {
      // "_" inside a word can neither open nor close an emphasis
      can_open = can_close = false;
    }
  }

  return {
    can_open: can_open,
    can_close: can_close,
    delims: count
  };
}

module.exports = function emphasis(state, silent) {
  var startCount,
      count,
      found,
      oldCount,
      newCount,
      stack,
      res,
      max = state.posMax,
      start = state.pos,
      marker = state.src.charCodeAt(start);

  if (marker !== 0x5F/* _ */ && marker !== 0x2A /* * */) { return false; }
  if (silent) { return false; } // don't run any pairs in validation mode

  res = scanDelims(state, start);
  startCount = res.delims;
  if (!res.can_open) {
    state.pos += startCount;
    // Earlier we checked !silent, but this implementation does not need it
    state.pending += state.src.slice(start, state.pos);
    return true;
  }

  state.pos = start + startCount;
  stack = [ startCount ];

  while (state.pos < max) {
    if (state.src.charCodeAt(state.pos) === marker) {
      res = scanDelims(state, state.pos);
      count = res.delims;
      if (res.can_close) {
        oldCount = stack.pop();
        newCount = count;

        while (oldCount !== newCount) {
          if (newCount < oldCount) {
            stack.push(oldCount - newCount);
            break;
          }

          // assert(newCount > oldCount)
          newCount -= oldCount;

          if (stack.length === 0) { break; }
          state.pos += oldCount;
          oldCount = stack.pop();
        }

        if (stack.length === 0) {
          startCount = oldCount;
          found = true;
          break;
        }
        state.pos += count;
        continue;
      }

      if (res.can_open) { stack.push(count); }
      state.pos += count;
      continue;
    }

    state.md.inline.skipToken(state);
  }

  if (!found) {
    // parser failed to find ending tag, so it's not valid emphasis
    state.pos = start;
    return false;
  }

  // found!
  state.posMax = state.pos;
  state.pos = start + startCount;

  // Earlier we checked !silent, but this implementation does not need it

  // we have `startCount` starting and ending markers,
  // now trying to serialize them into tokens
  for (count = startCount; count > 1; count -= 2) {
    state.push({ type: 'strong_open', level: state.level++ });
  }
  if (count % 2) { state.push({ type: 'em_open', level: state.level++ }); }

  state.md.inline.tokenize(state);

  if (count % 2) { state.push({ type: 'em_close', level: --state.level }); }
  for (count = startCount; count > 1; count -= 2) {
    state.push({ type: 'strong_close', level: --state.level });
  }

  state.pos = state.posMax + startCount;
  state.posMax = max;
  return true;
};
<|endoftext|>


<commit_before>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import styled from 'styled-components';

const StyledGridItem = styled.div`
  width: ${({ theme }) => theme.grid.desktop.itemWidth};
  height: ${({ theme }) => theme.grid.desktop.itemHeight};
  display: flex;
  flex-direction: column;

  @media ${({ theme }) => theme.breakpoint.tablet} {
    width: ${({ theme }) => theme.grid.tablet.itemWidth};
    height: ${({ theme }) => theme.grid.tablet.itemHeight};
  }

  @media ${({ theme }) => theme.breakpoint.mobile} {
    width: ${({ theme }) => theme.grid.mobile.itemWidth};
    height: ${({ theme }) => theme.grid.mobile.itemHeight};
  }

  @media ${({ theme }) => theme.breakpoint.min} {
    width: ${({ theme }) => theme.grid.min.itemWidth};
    height: ${({ theme }) => theme.grid.min.itemHeight};
  }
`;

const CardGridItem = ({ children, ...rest }) => (
  <StyledGridItem {...rest}>{children}</StyledGridItem>
);

CardGridItem.propTypes = {
  children: PropTypes.node,
};

export default CardGridItem;
export { default as CardPreviewContainer } from './card-preview';
export { default as CardTitle } from './card-title';
<commit_msg>no need for StyledCardItem, just passing through<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import styled from 'styled-components';

const CardGridItem = styled.div`
  width: ${({ theme }) => theme.grid.desktop.itemWidth};
  height: ${({ theme }) => theme.grid.desktop.itemHeight};
  display: flex;
  flex-direction: column;

  @media ${({ theme }) => theme.breakpoint.tablet} {
    width: ${({ theme }) => theme.grid.tablet.itemWidth};
    height: ${({ theme }) => theme.grid.tablet.itemHeight};
  }

  @media ${({ theme }) => theme.breakpoint.mobile} {
    width: ${({ theme }) => theme.grid.mobile.itemWidth};
    height: ${({ theme }) => theme.grid.mobile.itemHeight};
  }

  @media ${({ theme }) => theme.breakpoint.min} {
    width: ${({ theme }) => theme.grid.min.itemWidth};
    height: ${({ theme }) => theme.grid.min.itemHeight};
  }
`;

export default CardGridItem;
export { default as CardPreviewContainer } from './card-preview';
export { default as CardTitle } from './card-title';
<|endoftext|>


<commit_before>const _send = require('@tonybadguy/call-me-maybe');
const setup = require('./setup');
const cache = require('memory-cache');

const { getAccessToken, BASE_V3_URL } = setup;

// getAccessToken().then(data => console.log('works', data));
const Yelp = (function() {
    const businessSearch = `${BASE_V3_URL}/businesses/search`;
    const tempAccessToken =
        '6eJjO8MyHbd-wGHruAg_qEuY6sOQr1w14TKJ1s2CAkTXJb_famWPnQ_oMqnGDj06U-K77tPQaOohl2YXzU0coHjn8WzIoRbk0cJoHmgu45b-7JPSzvSKigpdsV2NWXYx';
    const accessTokenKey = 'accessToken';
    let accessTokenCache = new cache.Cache();
    let businessesCache = new cache.Cache();

    function init() {
        return new Promise((resolve, reject) => {
            if (!accessTokenCache.get('accessToken')) {
                getAccessToken()
                    .then(tokenStruct => {
                        // TODO: Uncoment code below for production
                        // const { access_token } = tokenStruct;
                        accessTokenCache.put(accessTokenKey, businessesCache);
                        resolve();
                    })
                    .catch(err => reject(err));
            }
        });
    }

    function searchBusiness(params) {
        return _send({
            url: businessSearch,
            query: params,
            bearerToken: accessTokenCache.get(accessTokenKey)
        }).then(response => response.jsonBody);
    }

    return {
        init,
        searchBusiness
    };
})();

const TEST_PARAMS = {
    term: 'shop',
    latitude: 37.773972,
    longitude: -122.431297
};

module.exports = Yelp;
<commit_msg>Made test use a temporary key manually<commit_after>const _send = require('@tonybadguy/call-me-maybe');
const setup = require('./setup');
const cache = require('memory-cache');

const { getAccessToken, BASE_V3_URL } = setup;

// getAccessToken().then(data => console.log('works', data));
const Yelp = (function() {
    const businessSearch = `${BASE_V3_URL}/businesses/search`;
    const tempAccessToken =
        '6eJjO8MyHbd-wGHruAg_qEuY6sOQr1w14TKJ1s2CAkTXJb_famWPnQ_oMqnGDj06U-K77tPQaOohl2YXzU0coHjn8WzIoRbk0cJoHmgu45b-7JPSzvSKigpdsV2NWXYx';
    const accessTokenKey = 'accessToken';
    let accessTokenCache = new cache.Cache();
    let businessesCache = new cache.Cache();

    function init() {
        return new Promise((resolve, reject) => {
            if (!accessTokenCache.get('accessToken')) {
                // getAccessToken()
                //     .then(tokenStruct => {
                //         const { access_token } = tokenStruct;
                //         accessTokenCache.put(accessTokenKey, access_token);
                //         resolve();
                //     })
                //     .catch(err => reject(err));
                accessTokenCache.put(accessTokenKey, tempAccessToken);
            }
            resolve();
        });
    }

    function searchBusiness(params) {
        return _send({
            url: businessSearch,
            query: params,
            bearerToken: accessTokenCache.get(accessTokenKey)
        }).then(response => response.jsonBody);
    }

    return {
        init,
        searchBusiness
    };
})();

const TEST_PARAMS = {
    term: 'shop',
    latitude: 37.773972,
    longitude: -122.431297
};

module.exports = Yelp;
<|endoftext|>


<commit_before>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

describe('CUJ: Creator can Add a Page Attachment', () => {
  describe('Action: Add Page Attachment', () => {
    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should allow adding Page Attachment with custom CTA Text');

    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should display warning for a link in the Page Attachment Area');

    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should display warning for an invalid link');
  });

  describe('Action: Remove Page Attachment', () => {
    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should allow removing a Page Attachment');
  });

  describe('Action: Transforming link with Page Attachment', () => {
    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should display tooltip for a link in Attachment area');

    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should cancel link transformation ending in Attachment area');
  });
});
<commit_msg>Start adding Karma tests.<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { Fixture } from './fixture';

describe('CUJ: Creator can Add a Page Attachment', () => {
  describe('Action: Add Page Attachment', () => {
    let fixture;
    const setPageAttachmentLink = async (link) => {
      const input = fixture.screen.getByLabelText('Edit: Page Attachment link');
      await fixture.events.click(input, { clickCount: 3 });
      await fixture.events.keyboard.type(link);
      await input.dispatchEvent(new window.Event('blur'));
    };

    const setCtaText = async (text) => {
      const input = fixture.screen.getByLabelText(
        'Edit: Page Attachment CTA text'
      );
      await fixture.events.click(input, { clickCount: 3 });
      await fixture.events.keyboard.type(text);
      await input.dispatchEvent(new window.Event('blur'));
    };

    const clickOnTarget = async (target) => {
      const { x, y, width, height } = target.getBoundingClientRect();
      await fixture.events.mouse.click(x + width / 2, y + height / 2);
    };

    beforeEach(async () => {
      fixture = new Fixture();
      await fixture.render();
      // Select Page by default.
      const safezone = fixture.querySelector('[data-testid="safezone"]');
      await clickOnTarget(safezone);
    });

    afterEach(() => {
      fixture.restore();
    });

    it('it should allow adding Page Attachment with custom CTA Text', async () => {
      await setPageAttachmentLink('http://example.com');
      await setCtaText('Click me!');
      const ctaText = fixture.screen.getByText('Click me!');
      expect(ctaText).toBeDefined();
    });

    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should display warning for a link in the Page Attachment Area');

    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should display warning for an invalid link');
  });

  describe('Action: Remove Page Attachment', () => {
    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should allow removing a Page Attachment');
  });

  describe('Action: Transforming link with Page Attachment', () => {
    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should display tooltip for a link in Attachment area');

    // Disable reason: Not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('it should cancel link transformation ending in Attachment area');
  });
});
<|endoftext|>


<commit_before>angular.module('tf2stadium')
.config(function($mdThemingProvider) {
  $mdThemingProvider.theme('default')
    .primaryPalette('light-blue')
    .accentPalette('deep-purple');
});<commit_msg>Changed theme to use colors #9cf and #4b9cd4.<commit_after>angular.module('tf2stadium')
.config(function($mdThemingProvider) {
  var darkBlueMap = $mdThemingProvider.extendPalette('light-blue', {
    '500': '4b9cd4'
  });
  var lightBlueMap = $mdThemingProvider.extendPalette('light-blue', {
    '500': '99ccff'
  });
  $mdThemingProvider.definePalette('darkBluePalette', darkBlueMap);
  $mdThemingProvider.definePalette('lightBluePalette', lightBlueMap);
  $mdThemingProvider.theme('default')
    .primaryPalette('darkBluePalette')
    .accentPalette('lightBluePalette');
});<|endoftext|>


<commit_before>;(function() {

  var db       = bc.namespace('db'),
      loc      = bc.namespace('location'),
      util     = bc.namespace('util'),
      Module,
      modules;

  Module = bc.namespace('Module', function Module(data) {
    if ( this instanceof Module === false ) {
      return new Module(data);
    }
    $.extend(true, this,
      {
        id: '',
        name: '',
        description: '',
        needs: [],
        wants: [],
        required: false,
        defaults: {
          enabled: true
        }
      },
      data,
      {
        data: {},
        state: null
      }
    );
    return this;
  });

  Module.prototype.clear = function() {
    this.data = {};
    db.remove(this.id);
  };

  Module.prototype.deps = function() {
    var needs = this.needs,
        wants = this.wants,
        deps  = {};
    util.forEach(util.concat(needs, wants), function(id) {
      var module = Module.get(id);
      if ( module.enabled() ) {
        deps[id] = module;
      }
    });
    return deps;
  };

  Module.prototype.get = function(key) {
    var data = this.data,
        defaults = this.defaults;
    if ( typeof key === 'string' ) {
      return data.hasOwnProperty(key) ? data[key] : defaults[key];
    } else {
      return $.extend(true, {}, defaults, data);
    }
  };

  Module.prototype.enabled = function(value) {
    if ( arguments.length === 0 ) {
      var needsEnabled = true;
      util.forEach(this.needs, function(id) {
        var module = Module.get(id);
        if ( !module.enabled() ) {
          needsEnabled = false;
        }
      });
      return this.required || (this.get('enabled') && needsEnabled);
    } else {
      this.set('enabled', value);
    }
  };

  Module.prototype.path = function(path) {
    path = path || '';
    if ( path.substring(0,1) === '/' ) {
      return loc.getResourcePath(path);
    }
    return loc.getResourcePath('/modules/'+this.id) + '/' + path;
  };

  Module.prototype.remove = function(key) {
    var self = this;
    delete self.data[key];
    db.get(self.id, function(data) {
      delete data[self.id][key];
      db.set(data);
    });
  };

  Module.prototype.set = function(key, value) {
    var self = this,
        data = self.data;
    if ( typeof key === 'object' ) {
      $.extend(true, data, key);
      db.get(self.id, function(data) {
        $.extend(true, data[self.id], key);
        db.set(data);
      });
    } else {
      data[key] = value;
      db.get(self.id, function(data) {
        data[self.id][key] = value;
        db.set(data);
      });
    }
  };

  Module.get = function(id) {
    return util.forEach(modules, function(module) {
      if ( module.id === id ) {
        return module;
      }
    });
  };

  Module.all = function() {
    return util.clone(modules);
  };

  (function() {
    var scripts = { all: [] },
        onContentScript = loc.onContentScript,
        domReady = onContentScript ? false : true, // no need to wait
        modulesReady = false,
        init = false;

    function checkReady() {
      if ( !modulesReady || !domReady ) {
        return false;
      }
      if ( !init && onContentScript ) {
        util.forEach(modules, function(module) {
          if ( module.enabled() ) {
            $('body').addClass('bc-'+module.id);
          }
        });
      }
      util.forEach(scripts.all, function(fn) {
        try {
          fn();
        } catch ( err ) {
          console.error(err);
        }
      });
      modules = modules.sort(function(a,b) {
        return (a.order || 0) - (b.order || 0);
      });
      util.forEach(modules, function(module) {
        if ( module.enabled() ) {
          util.forEach(scripts[module.id], function(fn) {
            try {
              fn(module, module.get());
            } catch ( err ) {
              console.error(err);
            }
          });
        }
      });
      if ( !init && onContentScript ) {
        $('body').css('visibility', 'visible');
      }
      init = true;
    }

    Module.ready = function(callback) {
      scripts.all.push(callback);
      checkReady();
    };

    Module.run = function(id, callback) {
      scripts[id] = scripts[id] || [];
      scripts[id].push(callback);
      checkReady();
    };

    bc.manifest(function(manifest) {
      modules = manifest.modules.map(function(data) {
        return new Module(data);
      });

      var count = modules.length;
      util.forEach(modules, function(module) {
        db.get(module.id, function(data) {
          $.extend(true, module.data, data[module.id] || {});
          count--;
          if ( count === 0 ) {
            modulesReady = true;
            checkReady();
          }
        });
      });
    });

    if ( onContentScript ) {
      $(function() {
        domReady = true;
        checkReady();
      });

      (function() {
        // For some reason, body stays visible unless we get really aggressive
        // like this and continually keep it hidden
        var hideBody;
        hideBody = function() {
          if ( init ) {
            return;
          }
          $('body').css('visibility', 'hidden');
          setTimeout(hideBody, 1);
        }
        hideBody();
      })();
    }
  })();

})();
<commit_msg>Fix setting db values when none exist yet<commit_after>;(function() {

  var db       = bc.namespace('db'),
      loc      = bc.namespace('location'),
      util     = bc.namespace('util'),
      Module,
      modules;

  Module = bc.namespace('Module', function Module(data) {
    if ( this instanceof Module === false ) {
      return new Module(data);
    }
    $.extend(true, this,
      {
        id: '',
        name: '',
        description: '',
        needs: [],
        wants: [],
        required: false,
        defaults: {
          enabled: true
        }
      },
      data,
      {
        data: {},
        state: null
      }
    );
    return this;
  });

  Module.prototype.clear = function() {
    this.data = {};
    db.remove(this.id);
  };

  Module.prototype.deps = function() {
    var needs = this.needs,
        wants = this.wants,
        deps  = {};
    util.forEach(util.concat(needs, wants), function(id) {
      var module = Module.get(id);
      if ( module.enabled() ) {
        deps[id] = module;
      }
    });
    return deps;
  };

  Module.prototype.get = function(key) {
    var data = this.data,
        defaults = this.defaults;
    if ( typeof key === 'string' ) {
      return data.hasOwnProperty(key) ? data[key] : defaults[key];
    } else {
      return $.extend(true, {}, defaults, data);
    }
  };

  Module.prototype.enabled = function(value) {
    if ( arguments.length === 0 ) {
      var needsEnabled = true;
      util.forEach(this.needs, function(id) {
        var module = Module.get(id);
        if ( !module.enabled() ) {
          needsEnabled = false;
        }
      });
      return this.required || (this.get('enabled') && needsEnabled);
    } else {
      this.set('enabled', value);
    }
  };

  Module.prototype.path = function(path) {
    path = path || '';
    if ( path.substring(0,1) === '/' ) {
      return loc.getResourcePath(path);
    }
    return loc.getResourcePath('/modules/'+this.id) + '/' + path;
  };

  Module.prototype.remove = function(key) {
    var self = this;
    delete self.data[key];
    db.get(self.id, function(data) {
      if ( data[self.id] ) {
        delete data[self.id][key];
        db.set(data);
      }
    });
  };

  Module.prototype.set = function(key, value) {
    var self = this,
        data = self.data;
    if ( typeof key === 'object' ) {
      $.extend(true, data, key);
      db.get(self.id, function(data) {
        $.extend(true, data[self.id] || {}, key);
        db.set(data);
      });
    } else {
      data[key] = value;
      db.get(self.id, function(data) {
        if ( !data[self.id] ) {
          data[self.id] = {};
        }
        data[self.id][key] = value;
        db.set(data);
      });
    }
  };

  Module.get = function(id) {
    return util.forEach(modules, function(module) {
      if ( module.id === id ) {
        return module;
      }
    });
  };

  Module.all = function() {
    return util.clone(modules);
  };

  (function() {
    var scripts = { all: [] },
        onContentScript = loc.onContentScript,
        domReady = onContentScript ? false : true, // no need to wait
        modulesReady = false,
        init = false;

    function checkReady() {
      if ( !modulesReady || !domReady ) {
        return false;
      }
      if ( !init && onContentScript ) {
        util.forEach(modules, function(module) {
          if ( module.enabled() ) {
            $('body').addClass('bc-'+module.id);
          }
        });
      }
      util.forEach(scripts.all, function(fn) {
        try {
          fn();
        } catch ( err ) {
          console.error(err);
        }
      });
      modules = modules.sort(function(a,b) {
        return (a.order || 0) - (b.order || 0);
      });
      util.forEach(modules, function(module) {
        if ( module.enabled() ) {
          util.forEach(scripts[module.id], function(fn) {
            try {
              fn(module, module.get());
            } catch ( err ) {
              console.error(err);
            }
          });
        }
      });
      if ( !init && onContentScript ) {
        $('body').css('visibility', 'visible');
      }
      init = true;
    }

    Module.ready = function(callback) {
      scripts.all.push(callback);
      checkReady();
    };

    Module.run = function(id, callback) {
      scripts[id] = scripts[id] || [];
      scripts[id].push(callback);
      checkReady();
    };

    bc.manifest(function(manifest) {
      modules = manifest.modules.map(function(data) {
        return new Module(data);
      });

      var count = modules.length;
      util.forEach(modules, function(module) {
        db.get(module.id, function(data) {
          $.extend(true, module.data, data[module.id] || {});
          count--;
          if ( count === 0 ) {
            modulesReady = true;
            checkReady();
          }
        });
      });
    });

    if ( onContentScript ) {
      $(function() {
        domReady = true;
        checkReady();
      });

      (function() {
        // For some reason, body stays visible unless we get really aggressive
        // like this and continually keep it hidden
        var hideBody;
        hideBody = function() {
          if ( init ) {
            return;
          }
          $('body').css('visibility', 'hidden');
          setTimeout(hideBody, 1);
        }
        hideBody();
      })();
    }
  })();

})();
<|endoftext|>