<commit_before>const _ = require('lodash');
const qs = require('query-string');
const LRUCache = require('lru-cache');
const logger = require('../../../logger');

const { fetch } = require('../../../../util');
const cleanUpEntrez = require('../clean-up-entrez');
const InvalidParamError = require('../../../../server/errors/invalid-param');
const { GPROFILER_URL, PC_CACHE_MAX_SIZE, COLLECTION_NAMESPACE_HGNC, COLLECTION_NAMESPACE_HGNC_SYMBOL, COLLECTION_NAMESPACE_UNIPROT, COLLECTION_NAMESPACE_NCBI_GENE, COLLECTION_NAMESPACE_ENSEMBL } = require('../../../../config');
const cache = require('../../../cache');
const GCONVERT_URL = GPROFILER_URL + 'gconvert.cgi';

const GPROFILER_COLLECTION_NAMESPACE_MAP = new Map([
  [COLLECTION_NAMESPACE_HGNC, 'HGNC_ACC'],
  [COLLECTION_NAMESPACE_HGNC_SYMBOL, 'HGNC'],
  [COLLECTION_NAMESPACE_UNIPROT, 'UNIPROTSWISSPROT'],
  [COLLECTION_NAMESPACE_NCBI_GENE, 'ENTREZGENE_ACC'],
  [COLLECTION_NAMESPACE_ENSEMBL, 'ENSG']
]);

const resultTemplate = ( unrecognized, duplicate, alias ) => {
  return {
    unrecognized: Array.from( unrecognized ) || [],
    duplicate: duplicate || {},
    alias: alias || {}
  };
};

const mapTarget = target  => {
  const gconvertNamespace = GPROFILER_COLLECTION_NAMESPACE_MAP.get( target );
  if( !gconvertNamespace ) throw new InvalidParamError( 'Unrecognized targetDb' );
  return gconvertNamespace;
};

const mapQuery = query => query.join(" ");

/*
 * mapParams
 * @param { object } params the input parameters
 */
const mapParams = params => {
  const target = mapTarget( params.target );
  const query = mapQuery( params.query );
  return _.assign({}, params, { target,  query });
};

/*
 * getForm
 * @param { array } query - Gene IDs
 * @param { object } userOptions
 * @returns { object } error and form
 */
const getForm = ( query, defaultOptions, userOptions ) => {

  let form = _.assign( {},
    defaultOptions,
    JSON.parse( JSON.stringify( userOptions ) ),
    { query: query }
  );

  if ( !Array.isArray( form.query ) ) {
    throw new InvalidParamError( 'Invalid query format' );
  }

  return mapParams( form );
};

const gConvertResponseHandler = body =>  {

  const entityInfoList = _.map(body.split('\n'), ele => { return ele.split('\t'); });
  entityInfoList.splice(-1, 1); // remove last element ''
  const unrecognized = new Set();
  let duplicate = {};
  const previous = new Map();
  let alias = {};
  const initialAliasIndex = 1;
  const convertedAliasIndex = 3;
  _.forEach(entityInfoList, info => {
    const convertedAlias = info[convertedAliasIndex];
    let initialAlias = info[initialAliasIndex];
    initialAlias = cleanUpEntrez(initialAlias);
    if (convertedAlias === 'N/A') {
      unrecognized.add(initialAlias);
    } else {
      if (!previous.has(convertedAlias)) {
        previous.set(convertedAlias, initialAlias);
      } else {
        if (!(convertedAlias in duplicate)) {
          duplicate[convertedAlias] = new Set([previous.get(convertedAlias)]);
        }
        duplicate[convertedAlias].add(initialAlias);
      }
      alias[initialAlias] = convertedAlias;
    }
  });
  for (const initialAlias in duplicate) {
    duplicate[initialAlias] = Array.from(duplicate[initialAlias]);
  }
  return resultTemplate( unrecognized, duplicate, alias );
};

/* rawValidatorGconvert
 * @param { array } query - identifier list query
 * @param { object } userOptions - options
 * @return { object } list of unrecognized, object with duplicated and list of mapped IDs
 */
const rawValidatorGconvert = async ( query, userOptions ) => {

  const defaultOptions = {
    'output': 'mini',
    'organism': 'hsapiens',
    'target': COLLECTION_NAMESPACE_HGNC,
    'prefix': 'ENTREZGENE_ACC'
  };

  try{
    const form = getForm( query, defaultOptions, userOptions );
    return fetch( GCONVERT_URL, {
      method: 'post',
      body: qs.stringify( form )
    })
    .then( response => response.text() )
    .then( gConvertResponseHandler );
  } catch ( err ) {
    logger.error(`Gprofiler convert query failed - ${ err }`);
    throw err;
  }
};

const pcCache = LRUCache({ max: PC_CACHE_MAX_SIZE, length: () => 1 });

const validatorGconvert = cache(rawValidatorGconvert, pcCache);

module.exports = { validatorGconvert,
  getForm,
  mapParams,
  gConvertResponseHandler,
  GPROFILER_COLLECTION_NAMESPACE_MAP
};
<commit_msg>dropped async on rawValidatorGconvert<commit_after>const _ = require('lodash');
const qs = require('query-string');
const LRUCache = require('lru-cache');
const logger = require('../../../logger');

const { fetch } = require('../../../../util');
const cleanUpEntrez = require('../clean-up-entrez');
const InvalidParamError = require('../../../../server/errors/invalid-param');
const { GPROFILER_URL, PC_CACHE_MAX_SIZE, COLLECTION_NAMESPACE_HGNC, COLLECTION_NAMESPACE_HGNC_SYMBOL, COLLECTION_NAMESPACE_UNIPROT, COLLECTION_NAMESPACE_NCBI_GENE, COLLECTION_NAMESPACE_ENSEMBL } = require('../../../../config');
const cache = require('../../../cache');
const GCONVERT_URL = GPROFILER_URL + 'gconvert.cgi';

const GPROFILER_COLLECTION_NAMESPACE_MAP = new Map([
  [COLLECTION_NAMESPACE_HGNC, 'HGNC_ACC'],
  [COLLECTION_NAMESPACE_HGNC_SYMBOL, 'HGNC'],
  [COLLECTION_NAMESPACE_UNIPROT, 'UNIPROTSWISSPROT'],
  [COLLECTION_NAMESPACE_NCBI_GENE, 'ENTREZGENE_ACC'],
  [COLLECTION_NAMESPACE_ENSEMBL, 'ENSG']
]);

const resultTemplate = ( unrecognized, duplicate, alias ) => {
  return {
    unrecognized: Array.from( unrecognized ) || [],
    duplicate: duplicate || {},
    alias: alias || {}
  };
};

const mapTarget = target  => {
  const gconvertNamespace = GPROFILER_COLLECTION_NAMESPACE_MAP.get( target );
  if( !gconvertNamespace ) throw new InvalidParamError( 'Unrecognized targetDb' );
  return gconvertNamespace;
};

const mapQuery = query => query.join(" ");

/*
 * mapParams
 * @param { object } params the input parameters
 */
const mapParams = params => {
  const target = mapTarget( params.target );
  const query = mapQuery( params.query );
  return _.assign({}, params, { target,  query });
};

/*
 * getForm
 * @param { array } query - Gene IDs
 * @param { object } userOptions
 * @returns { object } error and form
 */
const getForm = ( query, defaultOptions, userOptions ) => {

  let form = _.assign( {},
    defaultOptions,
    JSON.parse( JSON.stringify( userOptions ) ),
    { query: query }
  );

  if ( !Array.isArray( form.query ) ) {
    throw new InvalidParamError( 'Invalid query format' );
  }

  return mapParams( form );
};

const gConvertResponseHandler = body =>  {

  const entityInfoList = _.map(body.split('\n'), ele => { return ele.split('\t'); });
  entityInfoList.splice(-1, 1); // remove last element ''
  const unrecognized = new Set();
  let duplicate = {};
  const previous = new Map();
  let alias = {};
  const initialAliasIndex = 1;
  const convertedAliasIndex = 3;
  _.forEach(entityInfoList, info => {
    const convertedAlias = info[convertedAliasIndex];
    let initialAlias = info[initialAliasIndex];
    initialAlias = cleanUpEntrez(initialAlias);
    if (convertedAlias === 'N/A') {
      unrecognized.add(initialAlias);
    } else {
      if (!previous.has(convertedAlias)) {
        previous.set(convertedAlias, initialAlias);
      } else {
        if (!(convertedAlias in duplicate)) {
          duplicate[convertedAlias] = new Set([previous.get(convertedAlias)]);
        }
        duplicate[convertedAlias].add(initialAlias);
      }
      alias[initialAlias] = convertedAlias;
    }
  });
  for (const initialAlias in duplicate) {
    duplicate[initialAlias] = Array.from(duplicate[initialAlias]);
  }
  return resultTemplate( unrecognized, duplicate, alias );
};

/* rawValidatorGconvert
 * @param { array } query - identifier list query
 * @param { object } userOptions - options
 * @return { object } list of unrecognized, object with duplicated and list of mapped IDs
 */
const rawValidatorGconvert = ( query, userOptions ) => {

  const defaultOptions = {
    'output': 'mini',
    'organism': 'hsapiens',
    'target': COLLECTION_NAMESPACE_HGNC,
    'prefix': 'ENTREZGENE_ACC'
  };

  try{
    const form = getForm( query, defaultOptions, userOptions );
    return fetch( GCONVERT_URL, {
      method: 'post',
      body: qs.stringify( form )
    })
    .then( response => response.text() )
    .then( gConvertResponseHandler );
  } catch ( err ) {
    logger.error(`Gprofiler convert query failed - ${ err }`);
    throw err;
  }
};

const pcCache = LRUCache({ max: PC_CACHE_MAX_SIZE, length: () => 1 });

const validatorGconvert = cache(rawValidatorGconvert, pcCache);

module.exports = { validatorGconvert,
  getForm,
  mapParams,
  gConvertResponseHandler,
  GPROFILER_COLLECTION_NAMESPACE_MAP
};
<|endoftext|>


<commit_before>var timeLimit = Date.now() + 1000;

assert.equal((function f(n) {
  assert.operator(Date.now(), '<', timeLimit, "Timeout");
  if (n <= 0) {
    return "foo";
  }
  return f(n - 1);
})(1e6), "foo");
<commit_msg>Increase test timeout for Travis.<commit_after>var timeLimit = Date.now() + 5000;

assert.equal((function f(n) {
  assert.operator(Date.now(), '<', timeLimit, "Timeout");
  if (n <= 0) {
    return "foo";
  }
  return f(n - 1);
})(1e6), "foo");
<|endoftext|>


<commit_before>import fs from "fs-extra";
import path from "path";
import temp from "temp";
import { expect } from "chai";
import { stub } from "sinon";

import newApp from "../../../src/commands/new";
import npmDependencies from "../../../src/lib/npmDependencies";

import getRenderRequirementsFromEntrypoints from "../../../src/lib/server/getRenderRequirementsFromEntrypoints";

const MOCK_REQUEST = {
  headers: {
    host: "localhost:8888"
  }
};

describe("src/lib/server/getRenderRequirementsFromEntrypoints", () => {
  let originalCwd, tmpDir, fakeNpm, cwd, webpackAdditionsPath;

  beforeEach(() => {
    originalCwd = process.cwd();
    tmpDir = temp.mkdirSync("gluestick-new");
    fakeNpm = stub(npmDependencies, "install");
    process.chdir(tmpDir);

    newApp("test-app");
    const appDir = path.join(tmpDir, "test-app");
    process.chdir(appDir);
    cwd = process.cwd();
    webpackAdditionsPath = path.join(cwd, "src", "config", "webpack-additions.js");

    // Remove .babelrc so it wont complain about missing react preset
    fs.unlinkSync(path.join(cwd, ".babelrc"));
  });

  afterEach(() => {
    process.chdir(originalCwd);
    fs.removeSync(tmpDir);
    fakeNpm.restore();
  });

  it("return the default data if no entry points are defined no matter what the path", () => {
    const mockRequire = (path) => ({
      getStore: () => `getStore: ${path}`,
      default: `Contents of ${path}`
    });
    const request = { ...MOCK_REQUEST, url: "http://kook.com/todd" };
    const result = getRenderRequirementsFromEntrypoints(request, {}, mockRequire);
    const expectedResult = {
      Index: `Contents of ${cwd}/Index.js`,
      fileName: "main",
      getRoutes: `Contents of ${cwd}/src/config/routes`,
      store: `getStore: ${cwd}/src/config/.entries/main.js`
    };
    expect(result).to.deep.equal(expectedResult);
  });

  it("return the default data if no entry points are defined no matter what the path", () => {
    const mockRequire = (path) => ({
      getStore: () => `getStore: ${path}`,
      default: `Contents of ${path}`
    });
    const request = { ...MOCK_REQUEST, url: "http://kook.com/used-cars-for-sale" };
    const webpackAdditionsContent = "module.exports = { additionalLoaders: [], additionalPreLoaders: [], entryPoints: {'/used-cars-for-sale': { name: 'used'}}};";
    fs.outputFileSync(webpackAdditionsPath, webpackAdditionsContent);
    const result = getRenderRequirementsFromEntrypoints(request, {}, mockRequire);
    const expectedResult = {
      Index: `Contents of ${cwd}/Index.js`,
      fileName: "used",
      getRoutes: `Contents of ${cwd}/src/config/routes/used`,
      store: `getStore: ${cwd}/src/config/.entries/used.js`
    };
    expect(result).to.deep.equal(expectedResult);
  });
});

<commit_msg>fix test<commit_after>import fs from "fs-extra";
import path from "path";
import temp from "temp";
import { expect } from "chai";
import { stub, spy } from "sinon";

import newApp from "../../../src/commands/new";
import npmDependencies from "../../../src/lib/npmDependencies";

import getRenderRequirementsFromEntrypoints from "../../../src/lib/server/getRenderRequirementsFromEntrypoints";

const MOCK_REQUEST = {
  headers: {
    host: "localhost:8888"
  }
};

describe("src/lib/server/getRenderRequirementsFromEntrypoints", () => {
  let originalCwd, tmpDir, fakeNpm, cwd, webpackAdditionsPath, mockServerResponse;

  beforeEach(() => {
    originalCwd = process.cwd();
    tmpDir = temp.mkdirSync("gluestick-new");
    fakeNpm = stub(npmDependencies, "install");
    process.chdir(tmpDir);

    mockServerResponse = {
      append: spy()
    };

    newApp("test-app");
    const appDir = path.join(tmpDir, "test-app");
    process.chdir(appDir);
    cwd = process.cwd();
    webpackAdditionsPath = path.join(cwd, "src", "config", "webpack-additions.js");

    // Remove .babelrc so it wont complain about missing react preset
    fs.unlinkSync(path.join(cwd, ".babelrc"));
  });

  afterEach(() => {
    process.chdir(originalCwd);
    fs.removeSync(tmpDir);
    fakeNpm.restore();
  });

  it("return the default data if no entry points are defined no matter what the path", () => {
    const mockRequire = (path) => ({
      getStore: () => `getStore: ${path}`,
      default: `Contents of ${path}`
    });
    const request = { ...MOCK_REQUEST, url: "http://kook.com/todd" };
    const result = getRenderRequirementsFromEntrypoints(request, {}, mockServerResponse, mockRequire);
    const expectedResult = {
      Index: `Contents of ${cwd}/Index.js`,
      fileName: "main",
      getRoutes: `Contents of ${cwd}/src/config/routes`,
      store: `getStore: ${cwd}/src/config/.entries/main.js`
    };
    expect(result).to.deep.equal(expectedResult);
  });

  it("return the default data if no entry points are defined no matter what the path", () => {
    const mockRequire = (path) => ({
      getStore: () => `getStore: ${path}`,
      default: `Contents of ${path}`
    });
    const request = { ...MOCK_REQUEST, url: "http://kook.com/used-cars-for-sale" };
    const webpackAdditionsContent = "module.exports = { additionalLoaders: [], additionalPreLoaders: [], entryPoints: {'/used-cars-for-sale': { name: 'used'}}};";
    fs.outputFileSync(webpackAdditionsPath, webpackAdditionsContent);
    const result = getRenderRequirementsFromEntrypoints(request, {}, mockServerResponse, mockRequire);
    const expectedResult = {
      Index: `Contents of ${cwd}/Index.js`,
      fileName: "used",
      getRoutes: `Contents of ${cwd}/src/config/routes/used`,
      store: `getStore: ${cwd}/src/config/.entries/used.js`
    };
    expect(result).to.deep.equal(expectedResult);
  });
});

<|endoftext|>


<commit_before>// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var common = require('../common');
var assert = require('assert');

// Make sure that throwing in 'end' handler doesn't lock
// up the socket forever.
//
// This is NOT a good way to handle errors in general, but all
// the same, we should not be so brittle and easily broken.

var http = require('http');

var n = 0;
var server = http.createServer(function(req, res) {
  if (++n === 10) server.close();
  res.end('ok');
});

server.listen(common.PORT, function() {
  for (var i = 0; i < 10; i++) {
    var options = { port: common.PORT };

    var req = http.request(options, function (res) {
      res.resume()
      res.on('end', function() {
        throw new Error('gleep glorp');
      });
    });
    req.end();
  }
});

setTimeout(function() {
  process.removeListener('uncaughtException', catcher);
  throw new Error('Taking too long!');
}, 1000).unref();

process.on('uncaughtException', catcher);
var errors = 0;
function catcher() {
  errors++;
}

process.on('exit', function() {
  assert.equal(errors, 10);
  console.log('ok');
});
<commit_msg>modify test-http-end-throw-socket-handling's timeout<commit_after>// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var common = require('../common');
var assert = require('assert');

// Make sure that throwing in 'end' handler doesn't lock
// up the socket forever.
//
// This is NOT a good way to handle errors in general, but all
// the same, we should not be so brittle and easily broken.

var http = require('http');

var n = 0;
var server = http.createServer(function(req, res) {
  if (++n === 10) server.close();
  res.end('ok');
});

server.listen(common.PORT, function() {
  for (var i = 0; i < 10; i++) {
    var options = { port: common.PORT };

    var req = http.request(options, function (res) {
      res.resume()
      res.on('end', function() {
        throw new Error('gleep glorp');
      });
    });
    req.end();
  }
});

setTimeout(function() {
  process.removeListener('uncaughtException', catcher);
  throw new Error('Taking too long!');
}, 10000).unref();

process.on('uncaughtException', catcher);
var errors = 0;
function catcher() {
  errors++;
}

process.on('exit', function() {
  assert.equal(errors, 10);
  console.log('ok');
});
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import moment from 'moment';

import PantryPage from './PantryPage';
import { ApiRequest } from '../../services/ApiRequest';
import TopBar from '../TopBar/TopBar';

const DAYS_EXPIRES_SOON = 30;

export default class PantryPageContainer extends Component {
    constructor(props) {
        super(props);

        this.state = {
            pantry: null,
            errorMsg: null,
            removeItemId: null
        };

        this.onRemoveItem = this.onRemoveItem.bind(this);
    }

    populatePantry(pantryDb) {
        let itemsSortedByExpiration = pantryDb.pantryItems.sort((a, b) => new Date(a.expiration).getTime() - new Date(b.expiration).getTime());
        let allProductsList = {
            title: `All products (${itemsSortedByExpiration.length})`,
            items: itemsSortedByExpiration,
            key: 'allProducts'
        };

        let now = moment();
        let itemsThatWillExpireSoon = itemsSortedByExpiration.filter((item) => moment(item.expiration).diff(now, 'days') <= DAYS_EXPIRES_SOON)
        let porductsThatWillExpireSoonList = {
            title: `To expire soon (${itemsThatWillExpireSoon.length})`,
            items: itemsThatWillExpireSoon,
            key: 'willExpireSoon'
        };

        let pantry = {
            id: pantryDb.id,
            lists: [porductsThatWillExpireSoonList, allProductsList]
        };

        this.setState({ pantry });
    }

    onRemoveItem(itemId) {
        this.setState({ removeItemId: itemId });
    }

    removeItem(itemId) {
        this.setState({ removeItemId: null });

        ApiRequest.removePantryItem(itemId).then(response => {
            const {isSuccess, message} = response.body;
            if (!isSuccess) {
                console.log(message);
            }

            this.retrievePantryFromServer();
        }, err => {
            // TODO: also make sure to treat the forbidden requests
            console.log(err);
            this.setState({ errorMsg: 'There was an error with our servers. Please try again later!' });
        });
    }

    retrievePantryFromServer() {
        this.setState({ pantry: null });
        ApiRequest.getPantry().then(response => {
            let pantryDb = response.body;
            this.populatePantry(pantryDb);
        }, err => {
            console.log(err);
            this.setState({ errorMsg: 'There was an error with our servers. Please try again later!' });
        });
    }

    componentDidMount() {
        this.setState({ errorMsg: null });
        this.retrievePantryFromServer();
    }

    renderPantry() {
        if (this.state.errorMsg) {
            return
 {this.state.errorMsg}

        }

        if (this.state.pantry == null) {
            return (
 Loading...
);
        }

        return (<PantryPage
            pantry={this.state.pantry}
            onRemoveItem={this.onRemoveItem}
            router={this.props.router} />);
    }

    renderRemoveWindow() {
        if (this.state.removeItemId) {
            return (

                                Remove this product from the pantry?

                                <button
                                    className="pantry-remove-item__button"
                                    onClick={() => this.setState({ removeItemId: null })}>
                                    Cancel
                                </button>

                                <button
                                    className="pantry-remove-item__button pantry-remove-item__button--gray"
                                    onClick={() => this.removeItem(this.state.removeItemId)}>
                                    Remove
                                </button>

            );
        }
    }

    render() {
        return (

                <TopBar addButton addButtonOnClick={() => this.props.router.push(`/pantry/${this.state.pantry.id}/add-item`)} />
                {this.renderPantry()}

                {this.renderRemoveWindow()}

        );
    }

}<commit_msg>Refactor with clause guard<commit_after>import React, { Component } from 'react';
import moment from 'moment';

import PantryPage from './PantryPage';
import { ApiRequest } from '../../services/ApiRequest';
import TopBar from '../TopBar/TopBar';

const DAYS_EXPIRES_SOON = 30;

export default class PantryPageContainer extends Component {
    constructor(props) {
        super(props);

        this.state = {
            pantry: null,
            errorMsg: null,
            removeItemId: null
        };

        this.onRemoveItem = this.onRemoveItem.bind(this);
    }

    populatePantry(pantryDb) {
        let itemsSortedByExpiration = pantryDb.pantryItems.sort((a, b) => new Date(a.expiration).getTime() - new Date(b.expiration).getTime());
        let allProductsList = {
            title: `All products (${itemsSortedByExpiration.length})`,
            items: itemsSortedByExpiration,
            key: 'allProducts'
        };

        let now = moment();
        let itemsThatWillExpireSoon = itemsSortedByExpiration.filter((item) => moment(item.expiration).diff(now, 'days') <= DAYS_EXPIRES_SOON)
        let porductsThatWillExpireSoonList = {
            title: `To expire soon (${itemsThatWillExpireSoon.length})`,
            items: itemsThatWillExpireSoon,
            key: 'willExpireSoon'
        };

        let pantry = {
            id: pantryDb.id,
            lists: [porductsThatWillExpireSoonList, allProductsList]
        };

        this.setState({ pantry });
    }

    onRemoveItem(itemId) {
        this.setState({ removeItemId: itemId });
    }

    removeItem(itemId) {
        this.setState({ removeItemId: null });

        ApiRequest.removePantryItem(itemId).then(response => {
            const {isSuccess, message} = response.body;
            if (!isSuccess) {
                console.log(message);
            }

            this.retrievePantryFromServer();
        }, err => {
            // TODO: also make sure to treat the forbidden requests
            console.log(err);
            this.setState({ errorMsg: 'There was an error with our servers. Please try again later!' });
        });
    }

    retrievePantryFromServer() {
        this.setState({ pantry: null });
        ApiRequest.getPantry().then(response => {
            let pantryDb = response.body;
            this.populatePantry(pantryDb);
        }, err => {
            console.log(err);
            this.setState({ errorMsg: 'There was an error with our servers. Please try again later!' });
        });
    }

    componentDidMount() {
        this.setState({ errorMsg: null });
        this.retrievePantryFromServer();
    }

    renderPantry() {
        if (this.state.errorMsg) {
            return
 {this.state.errorMsg}

        }

        if (this.state.pantry == null) {
            return (
 Loading...
);
        }

        return (<PantryPage
            pantry={this.state.pantry}
            onRemoveItem={this.onRemoveItem}
            router={this.props.router} />);
    }

    renderRemoveWindow() {
        if (!this.state.removeItemId) {
            return null;
        }

        return (

                            Remove this product from the pantry?

                            <button
                                className="pantry-remove-item__button"
                                onClick={() => this.setState({ removeItemId: null })}>
                                Cancel
                                </button>

                            <button
                                className="pantry-remove-item__button pantry-remove-item__button--gray"
                                onClick={() => this.removeItem(this.state.removeItemId)}>
                                Remove
                                </button>

        );
    }

    render() {
        return (

                <TopBar addButton addButtonOnClick={() => this.props.router.push(`/pantry/${this.state.pantry.id}/add-item`)} />
                {this.renderPantry()}

                {this.renderRemoveWindow()}

        );
    }

}<|endoftext|>


<commit_before>
samesoundApp
    .controller('ListeningController',
        ['$scope', 'channel', 'MusicLoadingPolicy', 'config',
        function ($scope, channel, MusicLoadingPolicy, config) {
            $scope.playing = false;
            $scope.streaming = [];
            $scope.channel = channel;

            $scope.play = function (audio) {
                if (!audio) return false;
                $scope.playing = true;
                audio.play();
            }

            $scope.pause = function (audio) {
                if (!audio) return false;
                $scope.playing = false;
                audio.pause();
            }

            $scope.toogle = function () {
                var music = $scope.streaming[0];
                if (!music) return false;

                var audioTag = document.getElementById('music-' + music.Id);
                if (!audioTag) return false;

                if (!$scope.playing)
                    return $scope.play(audioTag);
                else
                    return $scope.pause(audioTag);
            }

            $scope.load = function (music) {
                if (MusicLoadingPolicy.shouldLoad(music)) {
                    music.Source = config.apiUrl + 'Musics/' + music.Id + '/Stream';
                    $scope.streaming.push(music);
                    MusicLoadingPolicy.load(music);
                }
            }

            for (var i = 0; i < $scope.channel.Musics.length; i++) {
                $scope.load($scope.channel.Musics[i]);
            }
        }])

samesoundApp
    .factory('MusicLoadingPolicy', function () {
        var maxMusicsLoadedAtOnce = 3,
            downloading = [];

        function load(music) {
            downloading.push(music);
        }

        function shouldLoad(music) {
            return maxMusicsLoadedAtOnce > downloading.length;
        }

        return {
            load: load,
            shouldLoad: shouldLoad
        };
    })
<commit_msg>Rename MusicLoadingPolicy -> MusicLoader Refactor factory Improve loading policy<commit_after>
samesoundApp
    .controller('ListeningController',
        ['$scope', 'channel', 'MusicLoader', 'config',
        function ($scope, channel, MusicLoader, config) {
            MusicLoader.initialize($scope);

            $scope.playing = false;
            $scope.streaming = [];
            $scope.channel = channel;

            $scope.play = function (audio) {
                if (!audio) return false;
                $scope.playing = true;
                audio.play();
            }

            $scope.pause = function (audio) {
                if (!audio) return false;
                $scope.playing = false;
                audio.pause();
            }

            $scope.toogle = function () {
                var music = $scope.streaming[0];
                if (!music) return false;

                var audioTag = document.getElementById('music-' + music.Id);
                if (!audioTag) return false;

                if (!$scope.playing)
                    return $scope.play(audioTag);
                else
                    return $scope.pause(audioTag);
            }

            $scope.tryToLoad = MusicLoader.tryToLoad

            var result = true, i = 0;
            while (i < $scope.channel.Musics.length && result) {
                var result = $scope.tryToLoad($scope.channel.Musics[i++]);
            }
        }])

samesoundApp
    .factory('MusicLoader', ['config', function (config) {
        var _$scope,
            maxMusicsLoadedAtOnce = 3,
            downloading = [];

        function initialize($scope) {
            _$scope = $scope;
            return this;
        }

        function tryToLoad(music, closure) {
            if (!shouldLoad(music))
                return false;

            downloading.push(music);
            music.Source = config.apiUrl + 'Musics/' + music.Id + '/Stream';
            _$scope.streaming.push(music);
        }

        // Defines the policy about sound loadings.
        function shouldLoad(music) {
            return maxMusicsLoadedAtOnce > downloading.length;
        }

        return {
            initialize: initialize,
            tryToLoad: tryToLoad,
            shouldLoad: shouldLoad
        };
    }])
<|endoftext|>


<commit_before>// TODO: This is quite ugly but seems reasonably fast? Maybe refactor
// this out before we merge into stable.
export function scrollTopFor(y) {
  let off = 0;
  for (let i=0; i<3; i++) {
    off = offsetCalculator(y - off);
  }
  return off;
}

export default function offsetCalculator(y) {
  const $header = $('header');
  const $title = $('#topic-title');
  const rawWinHeight = $(window).height();
  const windowHeight = rawWinHeight - $title.height();
  const expectedOffset = $title.height() - $header.find('.contents').height() + (windowHeight / 5);
  const ideal = $header.outerHeight(true) + ((expectedOffset < 0) ? 0 : expectedOffset);

  const $container = $('.posts-wrapper');
  const topPos = $container.offset().top;

  const scrollTop = y || $(window).scrollTop();
  const docHeight = $(document).height();
  const scrollPercent = (scrollTop / (docHeight-rawWinHeight));

  const inter = topPos - scrollTop + ($container.height() * scrollPercent);

  if (inter > ideal) {
    const bottom = $('#topic-bottom').offset().top;
    const switchPos = bottom - rawWinHeight;
    if (scrollTop > switchPos) {
      const p = Math.max(Math.min((scrollTop + inter - switchPos) / rawWinHeight, 1.0), 0.0);
      return ((1 - p) * ideal) + (p * inter);
    } else {
      return ideal;
    }
  }

  return inter;
}
<commit_msg>FIX: On small heights jumping to posts was iffy<commit_after>// TODO: This is quite ugly but seems reasonably fast? Maybe refactor
// this out before we merge into stable.
export function scrollTopFor(y) {
  let off = 0;
  for (let i=0; i<3; i++) {
    off = offsetCalculator(y - off);
  }
  return off;
}

export default function offsetCalculator(y) {
  const $header = $('header');
  const $title = $('#topic-title');
  const rawWinHeight = $(window).height();
  const windowHeight = rawWinHeight - $title.height();
  const eyeTarget = (windowHeight / 10);
  const headerHeight = $header.outerHeight(true);
  const expectedOffset = $title.height() - $header.find('.contents').height() + (eyeTarget * 2);
  const ideal = headerHeight + ((expectedOffset < 0) ? 0 : expectedOffset);

  const $container = $('.posts-wrapper');
  const topPos = $container.offset().top;

  const scrollTop = y || $(window).scrollTop();
  const docHeight = $(document).height();
  const scrollPercent = (scrollTop / (docHeight-rawWinHeight));

  let inter = topPos - scrollTop + ($container.height() * scrollPercent);
  if (inter < headerHeight + eyeTarget) {
    inter = headerHeight + eyeTarget;
  }

  if (inter > ideal) {
    const bottom = $('#topic-bottom').offset().top;
    const switchPos = bottom - rawWinHeight;
    if (scrollTop > switchPos) {
      const p = Math.max(Math.min((scrollTop + inter - switchPos) / rawWinHeight, 1.0), 0.0);
      return ((1 - p) * ideal) + (p * inter);
    } else {
      return ideal;
    }
  }

  return inter;
}
<|endoftext|>


<commit_before><commit_msg>filled in SimpleParameters<commit_after>//TODO: Require Files

Ext.namespace("Talho.Rollcall.ADST");

Talho.Rollcall.ADST.SimpleParameters = Ext.extend(Ext.Panel, {
  title:    "Simple Query Select",
  id:       "simple_query_select",
  itemId:   "simple_query_select",
  layout:   'auto',
  defaults: {
    xtype:  'container',
    layout: 'form',
    cls:    'ux-layout-auto-float-item',
    style:  {
      width:    '200px',
      minWidth: '200px'
    },
    defaults: {
      width: 200
    }
  },

  constructor: function (config) {
    this.items = [
      {xtype: 'combo', fieldLabel: 'Absenteeism', emptyText:'Gross', id: 'absent_simple', editable: false,
        store: new Ext.data.JsonStore({fields: ['id', 'value'], data: config.options.absenteeism})
      },
      {xtype: 'combo', fieldLabel: 'School District', emptyText:'Select School District...',
        allowBlank: true, id: 'school_district_simple', itemId: 'school_district_simple',
        displayField: 'name', editable: false,
        store: new Ext.data.JsonStore({fields: ['id', 'name'], data: config.options.school_districts}),
        listeners: {
          select: function(comboBox, record, index){
            Ext.getCmp('school_simple').clearValue();
            Ext.getCmp('school_type_simple').clearValue();
          }
        }
      },
      {xtype: 'combo', fieldLabel: 'School', emptyText:'Select School...', allowBlank: true,
        id: 'school_simple', itemId: 'school_simple', displayField: 'display_name', editable: false,
        store: new Ext.data.JsonStore({fields: ['id', 'display_name'], data: config.options.schools}),
        listeners: {
          select: function(comboBox, record, index){
            Ext.getCmp('school_district_simple').clearValue();
            Ext.getCmp('school_type_simple').clearValue();
            Ext.getCmp('return_individual_school_simple').setValue(true);
          }
        }
      },
      {xtype: 'combo', fieldLabel: 'School Type', emptyText:'Select School Type...', allowBlank: true,
        id: 'school_type_simple', itemId: 'school_type_simple', editable: false,
        store: new Ext.data.JsonStore({fields: ['id', 'value'], data: config.options.school_type}),
        listeners: {
          select: function(comboBox, record, index){
            Ext.getCmp('school_district_simple').clearValue();
            Ext.getCmp('school_simple').clearValue();
          }
        }
      },
      {xtype: 'datefield', fieldLabel: 'Start Date', name: 'startdt_simple', id: 'startdt_simple',
        endDateField: 'enddt_simple', emptyText:'Select Start Date...', allowBlank: true,
        selectOnFocus:true, ctCls: 'ux-combo-box-cls'
      },
      {xtype: 'datefield', fieldLabel: 'End Date', name: 'enddt_simple', id: 'enddt_simple',
        startDateField: 'startdt_simple', emptyText:'Select End Date...', allowBlank: true,
        selectOnFocus:true, ctCls: 'ux-combo-box-cls'
      },
      {xtype: 'combo', fieldLabel: 'Data Function', emptyText: 'Raw', id: 'data_func_simple', editable: false,
        store: new Ext.data.JsonStore({fields: ['id', 'value'], data: config.options.data_functions})
      },
      {xtype: 'container', cls: 'line-check-simple', items:[
        {xtype: 'checkbox', id: 'return_individual_school_simple', checked: true,
          boxLabel: "Return Individual School Results"
        }]
      },
      {xtype: 'container', cls: 'clear', items: [
        {xtype: 'button', text: "Switch to Advanced View >>", style:{margin: '0px 0px 5px 5px'}, scope: this,
          handler: function(buttonEl, eventObj) {
            this.hide();
            Ext.getCmp('rollcall_adst')._resetForm();
            Ext.getCmp('advanced_query_select').show();
            Ext.getCmp('advanced_query_select').doLayout();
          }
        }]
      }
    ];

    Talho.Rollcall.ADST.SimpleParameters.superclass.constructor.apply(this, config);
  }
});
<|endoftext|>


<commit_before>import { connect } from 'react-redux';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { createSelector } from 'reselect';
import { Map as ImmutableMap } from 'immutable';
import { useEmoji } from 'flavours/glitch/actions/emojis';
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { EmojiPicker as EmojiPickerAsync } from 'flavours/glitch/util/async-components';
import Overlay from 'react-overlays/lib/Overlay';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import detectPassiveEvents from 'detect-passive-events';
import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji';

const messages = defineMessages({
  emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
  emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
  emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°）╯︵ ┻━┻' },
  custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
  recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
  search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
  people: { id: 'emoji_button.people', defaultMessage: 'People' },
  nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
  food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
  activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
  travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
  objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
  symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
  flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
});

const perLine = 8;
const lines   = 2;

const DEFAULTS = [
  '+1',
  'grinning',
  'kissing_heart',
  'heart_eyes',
  'laughing',
  'stuck_out_tongue_winking_eye',
  'sweat_smile',
  'joy',
  'yum',
  'disappointed',
  'thinking_face',
  'weary',
  'sob',
  'sunglasses',
  'heart',
  'ok_hand',
];

const getFrequentlyUsedEmojis = createSelector([
  state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
], emojiCounters => {
  let emojis = emojiCounters
    .keySeq()
    .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
    .reverse()
    .slice(0, perLine * lines)
    .toArray();

  if (emojis.length < DEFAULTS.length) {
    emojis = emojis.concat(DEFAULTS.slice(0, DEFAULTS.length - emojis.length));
  }

  return emojis;
});

const getCustomEmojis = createSelector([
  state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
  const aShort = a.get('shortcode').toLowerCase();
  const bShort = b.get('shortcode').toLowerCase();

  if (aShort < bShort) {
    return -1;
  } else if (aShort > bShort ) {
    return 1;
  } else {
    return 0;
  }
}));

const mapStateToProps = state => ({
  custom_emojis: getCustomEmojis(state),
  skinTone: state.getIn(['settings', 'skinTone']),
  frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});

const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
  onSkinTone: skinTone => {
    dispatch(changeSetting(['skinTone'], skinTone));
  },

  onPickEmoji: emoji => {
    dispatch(useEmoji(emoji));

    if (onPickEmoji) {
      onPickEmoji(emoji);
    }
  },
});

const assetHost = process.env.CDN_HOST || '';
let EmojiPicker, Emoji; // load asynchronously

const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;

class ModifierPickerMenu extends React.PureComponent {

  static propTypes = {
    active: PropTypes.bool,
    onSelect: PropTypes.func.isRequired,
    onClose: PropTypes.func.isRequired,
  };

  handleClick = e => {
    this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.active) {
      this.attachListeners();
    } else {
      this.removeListeners();
    }
  }

  componentWillUnmount () {
    this.removeListeners();
  }

  handleDocumentClick = e => {
    if (this.node && !this.node.contains(e.target)) {
      this.props.onClose();
    }
  }

  attachListeners () {
    document.addEventListener('click', this.handleDocumentClick, false);
    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  removeListeners () {
    document.removeEventListener('click', this.handleDocumentClick, false);
    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  setRef = c => {
    this.node = c;
  }

  render () {
    const { active } = this.props;

    return (

        <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
        <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
        <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
        <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
        <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
        <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>

    );
  }

}

class ModifierPicker extends React.PureComponent {

  static propTypes = {
    active: PropTypes.bool,
    modifier: PropTypes.number,
    onChange: PropTypes.func,
    onClose: PropTypes.func,
    onOpen: PropTypes.func,
  };

  handleClick = () => {
    if (this.props.active) {
      this.props.onClose();
    } else {
      this.props.onOpen();
    }
  }

  handleSelect = modifier => {
    this.props.onChange(modifier);
    this.props.onClose();
  }

  render () {
    const { active, modifier } = this.props;

    return (

        <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
        <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />

    );
  }

}

@injectIntl
class EmojiPickerMenu extends React.PureComponent {

  static propTypes = {
    custom_emojis: ImmutablePropTypes.list,
    frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
    loading: PropTypes.bool,
    onClose: PropTypes.func.isRequired,
    onPick: PropTypes.func.isRequired,
    style: PropTypes.object,
    placement: PropTypes.string,
    arrowOffsetLeft: PropTypes.string,
    arrowOffsetTop: PropTypes.string,
    intl: PropTypes.object.isRequired,
    skinTone: PropTypes.number.isRequired,
    onSkinTone: PropTypes.func.isRequired,
  };

  static defaultProps = {
    style: {},
    loading: true,
    frequentlyUsedEmojis: [],
  };

  state = {
    modifierOpen: false,
    placement: null,
  };

  handleDocumentClick = e => {
    if (this.node && !this.node.contains(e.target)) {
      this.props.onClose();
    }
  }

  componentDidMount () {
    document.addEventListener('click', this.handleDocumentClick, false);
    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  componentWillUnmount () {
    document.removeEventListener('click', this.handleDocumentClick, false);
    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  setRef = c => {
    this.node = c;
  }

  getI18n = () => {
    const { intl } = this.props;

    return {
      search: intl.formatMessage(messages.emoji_search),
      notfound: intl.formatMessage(messages.emoji_not_found),
      categories: {
        search: intl.formatMessage(messages.search_results),
        recent: intl.formatMessage(messages.recent),
        people: intl.formatMessage(messages.people),
        nature: intl.formatMessage(messages.nature),
        foods: intl.formatMessage(messages.food),
        activity: intl.formatMessage(messages.activity),
        places: intl.formatMessage(messages.travel),
        objects: intl.formatMessage(messages.objects),
        symbols: intl.formatMessage(messages.symbols),
        flags: intl.formatMessage(messages.flags),
        custom: intl.formatMessage(messages.custom),
      },
    };
  }

  handleClick = emoji => {
    if (!emoji.native) {
      emoji.native = emoji.colons;
    }

    this.props.onClose();
    this.props.onPick(emoji);
  }

  handleModifierOpen = () => {
    this.setState({ modifierOpen: true });
  }

  handleModifierClose = () => {
    this.setState({ modifierOpen: false });
  }

  handleModifierChange = modifier => {
    this.props.onSkinTone(modifier);
  }

  render () {
    const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;

    if (loading) {
      return
;
    }

    const title = intl.formatMessage(messages.emoji);

    const { modifierOpen } = this.state;

    const categoriesSort = [
      'recent',
      'people',
      'nature',
      'foods',
      'activity',
      'places',
      'objects',
      'symbols',
      'flags',
    ];

    categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());

    return (

        <EmojiPicker
          perLine={8}
          emojiSize={22}
          sheetSize={32}
          custom={buildCustomEmojis(custom_emojis)}
          color=''
          emoji=''
          set='twitter'
          title={title}
          i18n={this.getI18n()}
          onClick={this.handleClick}
          include={categoriesSort}
          recent={frequentlyUsedEmojis}
          skin={skinTone}
          showPreview={false}
          backgroundImageFn={backgroundImageFn}
          autoFocus
          emojiTooltip
        />

        <ModifierPicker
          active={modifierOpen}
          modifier={skinTone}
          onOpen={this.handleModifierOpen}
          onClose={this.handleModifierClose}
          onChange={this.handleModifierChange}
        />

    );
  }

}

@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class EmojiPickerDropdown extends React.PureComponent {

  static propTypes = {
    custom_emojis: ImmutablePropTypes.list,
    frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
    intl: PropTypes.object.isRequired,
    onPickEmoji: PropTypes.func.isRequired,
    onSkinTone: PropTypes.func.isRequired,
    skinTone: PropTypes.number.isRequired,
  };

  state = {
    active: false,
    loading: false,
  };

  setRef = (c) => {
    this.dropdown = c;
  }

  onShowDropdown = ({ target }) => {
    this.setState({ active: true });

    if (!EmojiPicker) {
      this.setState({ loading: true });

      EmojiPickerAsync().then(EmojiMart => {
        EmojiPicker = EmojiMart.Picker;
        Emoji       = EmojiMart.Emoji;

        this.setState({ loading: false });
      }).catch(() => {
        this.setState({ loading: false });
      });
    }

    const { top } = target.getBoundingClientRect();
    this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
  }

  onHideDropdown = () => {
    this.setState({ active: false });
  }

  onToggle = (e) => {
    if (!this.state.loading && (!e.key || e.key === 'Enter')) {
      if (this.state.active) {
        this.onHideDropdown();
      } else {
        this.onShowDropdown(e);
      }
    }
  }

  handleKeyDown = e => {
    if (e.key === 'Escape') {
      this.onHideDropdown();
    }
  }

  setTargetRef = c => {
    this.target = c;
  }

  findTarget = () => {
    return this.target;
  }

  render () {
    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
    const title = intl.formatMessage(messages.emoji);
    const { active, loading, placement } = this.state;

    return (

          <img
            className={classNames('emojione', { 'pulse-loading': active && loading })}
            alt='🙂'
            src={`${assetHost}/emoji/1f602.svg`}
          />

        <Overlay show={active} placement={placement} target={this.findTarget}>
          <EmojiPickerMenu
            custom_emojis={this.props.custom_emojis}
            loading={loading}
            onClose={this.onHideDropdown}
            onPick={onPickEmoji}
            onSkinTone={onSkinTone}
            skinTone={skinTone}
            frequentlyUsedEmojis={frequentlyUsedEmojis}
          />
        </Overlay>

    );
  }

}
<commit_msg>Honor `system_emoji_font` in the Emoji Picker<commit_after>import { connect } from 'react-redux';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { createSelector } from 'reselect';
import { Map as ImmutableMap } from 'immutable';
import { useEmoji } from 'flavours/glitch/actions/emojis';
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { EmojiPicker as EmojiPickerAsync } from 'flavours/glitch/util/async-components';
import Overlay from 'react-overlays/lib/Overlay';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import detectPassiveEvents from 'detect-passive-events';
import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji';
import { useSystemEmojiFont } from 'flavours/glitch/util/initial_state';

const messages = defineMessages({
  emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
  emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
  emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°）╯︵ ┻━┻' },
  custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
  recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
  search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
  people: { id: 'emoji_button.people', defaultMessage: 'People' },
  nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
  food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
  activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
  travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
  objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
  symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
  flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
});

const perLine = 8;
const lines   = 2;

const DEFAULTS = [
  '+1',
  'grinning',
  'kissing_heart',
  'heart_eyes',
  'laughing',
  'stuck_out_tongue_winking_eye',
  'sweat_smile',
  'joy',
  'yum',
  'disappointed',
  'thinking_face',
  'weary',
  'sob',
  'sunglasses',
  'heart',
  'ok_hand',
];

const getFrequentlyUsedEmojis = createSelector([
  state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
], emojiCounters => {
  let emojis = emojiCounters
    .keySeq()
    .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
    .reverse()
    .slice(0, perLine * lines)
    .toArray();

  if (emojis.length < DEFAULTS.length) {
    emojis = emojis.concat(DEFAULTS.slice(0, DEFAULTS.length - emojis.length));
  }

  return emojis;
});

const getCustomEmojis = createSelector([
  state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
  const aShort = a.get('shortcode').toLowerCase();
  const bShort = b.get('shortcode').toLowerCase();

  if (aShort < bShort) {
    return -1;
  } else if (aShort > bShort ) {
    return 1;
  } else {
    return 0;
  }
}));

const mapStateToProps = state => ({
  custom_emojis: getCustomEmojis(state),
  skinTone: state.getIn(['settings', 'skinTone']),
  frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});

const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
  onSkinTone: skinTone => {
    dispatch(changeSetting(['skinTone'], skinTone));
  },

  onPickEmoji: emoji => {
    dispatch(useEmoji(emoji));

    if (onPickEmoji) {
      onPickEmoji(emoji);
    }
  },
});

const assetHost = process.env.CDN_HOST || '';
let EmojiPicker, Emoji; // load asynchronously

const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;

class ModifierPickerMenu extends React.PureComponent {

  static propTypes = {
    active: PropTypes.bool,
    onSelect: PropTypes.func.isRequired,
    onClose: PropTypes.func.isRequired,
  };

  handleClick = e => {
    this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.active) {
      this.attachListeners();
    } else {
      this.removeListeners();
    }
  }

  componentWillUnmount () {
    this.removeListeners();
  }

  handleDocumentClick = e => {
    if (this.node && !this.node.contains(e.target)) {
      this.props.onClose();
    }
  }

  attachListeners () {
    document.addEventListener('click', this.handleDocumentClick, false);
    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  removeListeners () {
    document.removeEventListener('click', this.handleDocumentClick, false);
    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  setRef = c => {
    this.node = c;
  }

  render () {
    const { active } = this.props;

    return (

        <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
        <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
        <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
        <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
        <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
        <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>

    );
  }

}

class ModifierPicker extends React.PureComponent {

  static propTypes = {
    active: PropTypes.bool,
    modifier: PropTypes.number,
    onChange: PropTypes.func,
    onClose: PropTypes.func,
    onOpen: PropTypes.func,
  };

  handleClick = () => {
    if (this.props.active) {
      this.props.onClose();
    } else {
      this.props.onOpen();
    }
  }

  handleSelect = modifier => {
    this.props.onChange(modifier);
    this.props.onClose();
  }

  render () {
    const { active, modifier } = this.props;

    return (

        <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
        <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />

    );
  }

}

@injectIntl
class EmojiPickerMenu extends React.PureComponent {

  static propTypes = {
    custom_emojis: ImmutablePropTypes.list,
    frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
    loading: PropTypes.bool,
    onClose: PropTypes.func.isRequired,
    onPick: PropTypes.func.isRequired,
    style: PropTypes.object,
    placement: PropTypes.string,
    arrowOffsetLeft: PropTypes.string,
    arrowOffsetTop: PropTypes.string,
    intl: PropTypes.object.isRequired,
    skinTone: PropTypes.number.isRequired,
    onSkinTone: PropTypes.func.isRequired,
  };

  static defaultProps = {
    style: {},
    loading: true,
    frequentlyUsedEmojis: [],
  };

  state = {
    modifierOpen: false,
    placement: null,
  };

  handleDocumentClick = e => {
    if (this.node && !this.node.contains(e.target)) {
      this.props.onClose();
    }
  }

  componentDidMount () {
    document.addEventListener('click', this.handleDocumentClick, false);
    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  componentWillUnmount () {
    document.removeEventListener('click', this.handleDocumentClick, false);
    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
  }

  setRef = c => {
    this.node = c;
  }

  getI18n = () => {
    const { intl } = this.props;

    return {
      search: intl.formatMessage(messages.emoji_search),
      notfound: intl.formatMessage(messages.emoji_not_found),
      categories: {
        search: intl.formatMessage(messages.search_results),
        recent: intl.formatMessage(messages.recent),
        people: intl.formatMessage(messages.people),
        nature: intl.formatMessage(messages.nature),
        foods: intl.formatMessage(messages.food),
        activity: intl.formatMessage(messages.activity),
        places: intl.formatMessage(messages.travel),
        objects: intl.formatMessage(messages.objects),
        symbols: intl.formatMessage(messages.symbols),
        flags: intl.formatMessage(messages.flags),
        custom: intl.formatMessage(messages.custom),
      },
    };
  }

  handleClick = emoji => {
    if (!emoji.native) {
      emoji.native = emoji.colons;
    }

    this.props.onClose();
    this.props.onPick(emoji);
  }

  handleModifierOpen = () => {
    this.setState({ modifierOpen: true });
  }

  handleModifierClose = () => {
    this.setState({ modifierOpen: false });
  }

  handleModifierChange = modifier => {
    this.props.onSkinTone(modifier);
  }

  render () {
    const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;

    if (loading) {
      return
;
    }

    const title = intl.formatMessage(messages.emoji);

    const { modifierOpen } = this.state;

    const categoriesSort = [
      'recent',
      'people',
      'nature',
      'foods',
      'activity',
      'places',
      'objects',
      'symbols',
      'flags',
    ];

    categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());

    return (

        <EmojiPicker
          perLine={8}
          emojiSize={22}
          sheetSize={32}
          custom={buildCustomEmojis(custom_emojis)}
          color=''
          emoji=''
          set='twitter'
          title={title}
          i18n={this.getI18n()}
          onClick={this.handleClick}
          include={categoriesSort}
          recent={frequentlyUsedEmojis}
          skin={skinTone}
          showPreview={false}
          backgroundImageFn={backgroundImageFn}
          autoFocus
          emojiTooltip
          native={useSystemEmojiFont}
        />

        <ModifierPicker
          active={modifierOpen}
          modifier={skinTone}
          onOpen={this.handleModifierOpen}
          onClose={this.handleModifierClose}
          onChange={this.handleModifierChange}
        />

    );
  }

}

@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class EmojiPickerDropdown extends React.PureComponent {

  static propTypes = {
    custom_emojis: ImmutablePropTypes.list,
    frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
    intl: PropTypes.object.isRequired,
    onPickEmoji: PropTypes.func.isRequired,
    onSkinTone: PropTypes.func.isRequired,
    skinTone: PropTypes.number.isRequired,
  };

  state = {
    active: false,
    loading: false,
  };

  setRef = (c) => {
    this.dropdown = c;
  }

  onShowDropdown = ({ target }) => {
    this.setState({ active: true });

    if (!EmojiPicker) {
      this.setState({ loading: true });

      EmojiPickerAsync().then(EmojiMart => {
        EmojiPicker = EmojiMart.Picker;
        Emoji       = EmojiMart.Emoji;

        this.setState({ loading: false });
      }).catch(() => {
        this.setState({ loading: false });
      });
    }

    const { top } = target.getBoundingClientRect();
    this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
  }

  onHideDropdown = () => {
    this.setState({ active: false });
  }

  onToggle = (e) => {
    if (!this.state.loading && (!e.key || e.key === 'Enter')) {
      if (this.state.active) {
        this.onHideDropdown();
      } else {
        this.onShowDropdown(e);
      }
    }
  }

  handleKeyDown = e => {
    if (e.key === 'Escape') {
      this.onHideDropdown();
    }
  }

  setTargetRef = c => {
    this.target = c;
  }

  findTarget = () => {
    return this.target;
  }

  render () {
    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
    const title = intl.formatMessage(messages.emoji);
    const { active, loading, placement } = this.state;

    return (

          <img
            className={classNames('emojione', { 'pulse-loading': active && loading })}
            alt='🙂'
            src={`${assetHost}/emoji/1f602.svg`}
          />

        <Overlay show={active} placement={placement} target={this.findTarget}>
          <EmojiPickerMenu
            custom_emojis={this.props.custom_emojis}
            loading={loading}
            onClose={this.onHideDropdown}
            onPick={onPickEmoji}
            onSkinTone={onSkinTone}
            skinTone={skinTone}
            frequentlyUsedEmojis={frequentlyUsedEmojis}
          />
        </Overlay>

    );
  }

}
<|endoftext|>


<commit_before>/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
 *
 * 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
 *
 *     http://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.
 */
/* globals scrollIntoView, HandTool, PDFViewerApplication */

'use strict';

var DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms
var SELECTOR = 'presentationControls';
var DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1000; // in ms

var PresentationMode = {
  active: false,
  args: null,
  contextMenuOpen: false,
//#if (GENERIC || CHROME)
  prevCoords: { x: null, y: null },
//#endif

  initialize: function presentationModeInitialize(options) {
    this.container = options.container;
    this.secondaryToolbar = options.secondaryToolbar;

    this.viewer = this.container.firstElementChild;

    this.firstPage = options.firstPage;
    this.lastPage = options.lastPage;
    this.pageRotateCw = options.pageRotateCw;
    this.pageRotateCcw = options.pageRotateCcw;

    this.firstPage.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.firstPageClick();
    }.bind(this));
    this.lastPage.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.lastPageClick();
    }.bind(this));

    this.pageRotateCw.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.pageRotateCwClick();
    }.bind(this));
    this.pageRotateCcw.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.pageRotateCcwClick();
    }.bind(this));
  },

  get isFullscreen() {
    return (document.fullscreenElement ||
            document.mozFullScreen ||
            document.webkitIsFullScreen ||
            document.msFullscreenElement);
  },

  /**
   * Initialize a timeout that is used to specify switchInProgress when the
   * browser transitions to fullscreen mode. Since resize events are triggered
   * multiple times during the switch to fullscreen mode, this is necessary in
   * order to prevent the page from being scrolled partially, or completely,
   * out of view when Presentation Mode is enabled.
   * Note: This is only an issue at certain zoom levels, e.g. 'page-width'.
   */
  _setSwitchInProgress: function presentationMode_setSwitchInProgress() {
    if (this.switchInProgress) {
      clearTimeout(this.switchInProgress);
    }
    this.switchInProgress = setTimeout(function switchInProgressTimeout() {
      delete this.switchInProgress;
      this._notifyStateChange();
    }.bind(this), DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS);
  },

  _resetSwitchInProgress: function presentationMode_resetSwitchInProgress() {
    if (this.switchInProgress) {
      clearTimeout(this.switchInProgress);
      delete this.switchInProgress;
    }
  },

  request: function presentationModeRequest() {
    if (!PDFViewerApplication.supportsFullscreen || this.isFullscreen ||
        !this.viewer.hasChildNodes()) {
      return false;
    }
    this._setSwitchInProgress();
    this._notifyStateChange();

    if (this.container.requestFullscreen) {
      this.container.requestFullscreen();
    } else if (this.container.mozRequestFullScreen) {
      this.container.mozRequestFullScreen();
    } else if (this.container.webkitRequestFullScreen) {
      this.container.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
    } else if (this.container.msRequestFullscreen) {
      this.container.msRequestFullscreen();
    } else {
      return false;
    }

    this.args = {
      page: PDFViewerApplication.page,
      previousScale: PDFViewerApplication.currentScaleValue
    };

    return true;
  },

  _notifyStateChange: function presentationModeNotifyStateChange() {
    var event = document.createEvent('CustomEvent');
    event.initCustomEvent('presentationmodechanged', true, true, {
      active: PresentationMode.active,
      switchInProgress: !!PresentationMode.switchInProgress
    });
    window.dispatchEvent(event);
  },

  enter: function presentationModeEnter() {
    this.active = true;
    this._resetSwitchInProgress();
    this._notifyStateChange();

    // Ensure that the correct page is scrolled into view when entering
    // Presentation Mode, by waiting until fullscreen mode in enabled.
    // Note: This is only necessary in non-Mozilla browsers.
    setTimeout(function enterPresentationModeTimeout() {
      PDFViewerApplication.page = this.args.page;
      PDFViewerApplication.setScale('page-fit', true);
    }.bind(this), 0);

    window.addEventListener('mousemove', this.mouseMove, false);
    window.addEventListener('mousedown', this.mouseDown, false);
    window.addEventListener('contextmenu', this.contextMenu, false);

    this.showControls();
    HandTool.enterPresentationMode();
    this.contextMenuOpen = false;
    this.container.setAttribute('contextmenu', 'viewerContextMenu');
  },

  exit: function presentationModeExit() {
    var page = PDFViewerApplication.page;

    // Ensure that the correct page is scrolled into view when exiting
    // Presentation Mode, by waiting until fullscreen mode is disabled.
    // Note: This is only necessary in non-Mozilla browsers.
    setTimeout(function exitPresentationModeTimeout() {
      this.active = false;
      this._notifyStateChange();

      PDFViewerApplication.setScale(this.args.previousScale, true);
      PDFViewerApplication.page = page;
      this.args = null;
    }.bind(this), 0);

    window.removeEventListener('mousemove', this.mouseMove, false);
    window.removeEventListener('mousedown', this.mouseDown, false);
    window.removeEventListener('contextmenu', this.contextMenu, false);

    this.hideControls();
    PDFViewerApplication.clearMouseScrollState();
    HandTool.exitPresentationMode();
    this.container.removeAttribute('contextmenu');
    this.contextMenuOpen = false;

    // Ensure that the thumbnail of the current page is visible
    // when exiting presentation mode.
    scrollIntoView(document.getElementById('thumbnailContainer' + page));
  },

  showControls: function presentationModeShowControls() {
    if (this.controlsTimeout) {
      clearTimeout(this.controlsTimeout);
    } else {
      this.container.classList.add(SELECTOR);
    }
    this.controlsTimeout = setTimeout(function hideControlsTimeout() {
      this.container.classList.remove(SELECTOR);
      delete this.controlsTimeout;
    }.bind(this), DELAY_BEFORE_HIDING_CONTROLS);
  },

  hideControls: function presentationModeHideControls() {
    if (!this.controlsTimeout) {
      return;
    }
    this.container.classList.remove(SELECTOR);
    clearTimeout(this.controlsTimeout);
    delete this.controlsTimeout;
  },

  mouseMove: function presentationModeMouseMove(evt) {
//#if (GENERIC || CHROME)
    // Workaround for a bug in WebKit browsers that causes the 'mousemove' event
    // to be fired when the cursor is changed. For details, see:
    // http://code.google.com/p/chromium/issues/detail?id=103041.

    var currCoords = { x: evt.clientX, y: evt.clientY };
    var prevCoords = PresentationMode.prevCoords;
    PresentationMode.prevCoords = currCoords;

    if (currCoords.x === prevCoords.x && currCoords.y === prevCoords.y) {
      return;
    }
//#endif
    PresentationMode.showControls();
  },

  mouseDown: function presentationModeMouseDown(evt) {
    var self = PresentationMode;
    if (self.contextMenuOpen) {
      self.contextMenuOpen = false;
      evt.preventDefault();
      return;
    }

    if (evt.button === 0) {
      // Enable clicking of links in presentation mode. Please note:
      // Only links pointing to destinations in the current PDF document work.
      var isInternalLink = (evt.target.href &&
                            evt.target.classList.contains('internalLink'));
      if (!isInternalLink) {
        // Unless an internal link was clicked, advance one page.
        evt.preventDefault();
        PDFViewerApplication.page += (evt.shiftKey ? -1 : 1);
      }
    }
  },

  contextMenu: function presentationModeContextMenu(evt) {
    PresentationMode.contextMenuOpen = true;
  }
};

(function presentationModeClosure() {
  function presentationModeChange(e) {
    if (PresentationMode.isFullscreen) {
      PresentationMode.enter();
    } else {
      PresentationMode.exit();
    }
  }

  window.addEventListener('fullscreenchange', presentationModeChange, false);
  window.addEventListener('mozfullscreenchange', presentationModeChange, false);
  window.addEventListener('webkitfullscreenchange', presentationModeChange,
                          false);
  window.addEventListener('MSFullscreenChange', presentationModeChange, false);
})();
<commit_msg>Remove any active text selection when entering Presentation Mode<commit_after>/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
 *
 * 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
 *
 *     http://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.
 */
/* globals scrollIntoView, HandTool, PDFViewerApplication */

'use strict';

var DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms
var SELECTOR = 'presentationControls';
var DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1000; // in ms

var PresentationMode = {
  active: false,
  args: null,
  contextMenuOpen: false,
//#if (GENERIC || CHROME)
  prevCoords: { x: null, y: null },
//#endif

  initialize: function presentationModeInitialize(options) {
    this.container = options.container;
    this.secondaryToolbar = options.secondaryToolbar;

    this.viewer = this.container.firstElementChild;

    this.firstPage = options.firstPage;
    this.lastPage = options.lastPage;
    this.pageRotateCw = options.pageRotateCw;
    this.pageRotateCcw = options.pageRotateCcw;

    this.firstPage.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.firstPageClick();
    }.bind(this));
    this.lastPage.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.lastPageClick();
    }.bind(this));

    this.pageRotateCw.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.pageRotateCwClick();
    }.bind(this));
    this.pageRotateCcw.addEventListener('click', function() {
      this.contextMenuOpen = false;
      this.secondaryToolbar.pageRotateCcwClick();
    }.bind(this));
  },

  get isFullscreen() {
    return (document.fullscreenElement ||
            document.mozFullScreen ||
            document.webkitIsFullScreen ||
            document.msFullscreenElement);
  },

  /**
   * Initialize a timeout that is used to specify switchInProgress when the
   * browser transitions to fullscreen mode. Since resize events are triggered
   * multiple times during the switch to fullscreen mode, this is necessary in
   * order to prevent the page from being scrolled partially, or completely,
   * out of view when Presentation Mode is enabled.
   * Note: This is only an issue at certain zoom levels, e.g. 'page-width'.
   */
  _setSwitchInProgress: function presentationMode_setSwitchInProgress() {
    if (this.switchInProgress) {
      clearTimeout(this.switchInProgress);
    }
    this.switchInProgress = setTimeout(function switchInProgressTimeout() {
      delete this.switchInProgress;
      this._notifyStateChange();
    }.bind(this), DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS);
  },

  _resetSwitchInProgress: function presentationMode_resetSwitchInProgress() {
    if (this.switchInProgress) {
      clearTimeout(this.switchInProgress);
      delete this.switchInProgress;
    }
  },

  request: function presentationModeRequest() {
    if (!PDFViewerApplication.supportsFullscreen || this.isFullscreen ||
        !this.viewer.hasChildNodes()) {
      return false;
    }
    this._setSwitchInProgress();
    this._notifyStateChange();

    if (this.container.requestFullscreen) {
      this.container.requestFullscreen();
    } else if (this.container.mozRequestFullScreen) {
      this.container.mozRequestFullScreen();
    } else if (this.container.webkitRequestFullScreen) {
      this.container.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
    } else if (this.container.msRequestFullscreen) {
      this.container.msRequestFullscreen();
    } else {
      return false;
    }

    this.args = {
      page: PDFViewerApplication.page,
      previousScale: PDFViewerApplication.currentScaleValue
    };

    return true;
  },

  _notifyStateChange: function presentationModeNotifyStateChange() {
    var event = document.createEvent('CustomEvent');
    event.initCustomEvent('presentationmodechanged', true, true, {
      active: PresentationMode.active,
      switchInProgress: !!PresentationMode.switchInProgress
    });
    window.dispatchEvent(event);
  },

  enter: function presentationModeEnter() {
    this.active = true;
    this._resetSwitchInProgress();
    this._notifyStateChange();

    // Ensure that the correct page is scrolled into view when entering
    // Presentation Mode, by waiting until fullscreen mode in enabled.
    // Note: This is only necessary in non-Mozilla browsers.
    setTimeout(function enterPresentationModeTimeout() {
      PDFViewerApplication.page = this.args.page;
      PDFViewerApplication.setScale('page-fit', true);
    }.bind(this), 0);

    window.addEventListener('mousemove', this.mouseMove, false);
    window.addEventListener('mousedown', this.mouseDown, false);
    window.addEventListener('contextmenu', this.contextMenu, false);

    this.showControls();
    HandTool.enterPresentationMode();
    this.contextMenuOpen = false;
    this.container.setAttribute('contextmenu', 'viewerContextMenu');

    // Text selection is disabled in Presentation Mode, thus it's not possible
    // for the user to deselect text that is selected (e.g. with "Select all")
    // when entering Presentation Mode, hence we remove any active selection.
    window.getSelection().removeAllRanges();
  },

  exit: function presentationModeExit() {
    var page = PDFViewerApplication.page;

    // Ensure that the correct page is scrolled into view when exiting
    // Presentation Mode, by waiting until fullscreen mode is disabled.
    // Note: This is only necessary in non-Mozilla browsers.
    setTimeout(function exitPresentationModeTimeout() {
      this.active = false;
      this._notifyStateChange();

      PDFViewerApplication.setScale(this.args.previousScale, true);
      PDFViewerApplication.page = page;
      this.args = null;
    }.bind(this), 0);

    window.removeEventListener('mousemove', this.mouseMove, false);
    window.removeEventListener('mousedown', this.mouseDown, false);
    window.removeEventListener('contextmenu', this.contextMenu, false);

    this.hideControls();
    PDFViewerApplication.clearMouseScrollState();
    HandTool.exitPresentationMode();
    this.container.removeAttribute('contextmenu');
    this.contextMenuOpen = false;

    // Ensure that the thumbnail of the current page is visible
    // when exiting presentation mode.
    scrollIntoView(document.getElementById('thumbnailContainer' + page));
  },

  showControls: function presentationModeShowControls() {
    if (this.controlsTimeout) {
      clearTimeout(this.controlsTimeout);
    } else {
      this.container.classList.add(SELECTOR);
    }
    this.controlsTimeout = setTimeout(function hideControlsTimeout() {
      this.container.classList.remove(SELECTOR);
      delete this.controlsTimeout;
    }.bind(this), DELAY_BEFORE_HIDING_CONTROLS);
  },

  hideControls: function presentationModeHideControls() {
    if (!this.controlsTimeout) {
      return;
    }
    this.container.classList.remove(SELECTOR);
    clearTimeout(this.controlsTimeout);
    delete this.controlsTimeout;
  },

  mouseMove: function presentationModeMouseMove(evt) {
//#if (GENERIC || CHROME)
    // Workaround for a bug in WebKit browsers that causes the 'mousemove' event
    // to be fired when the cursor is changed. For details, see:
    // http://code.google.com/p/chromium/issues/detail?id=103041.

    var currCoords = { x: evt.clientX, y: evt.clientY };
    var prevCoords = PresentationMode.prevCoords;
    PresentationMode.prevCoords = currCoords;

    if (currCoords.x === prevCoords.x && currCoords.y === prevCoords.y) {
      return;
    }
//#endif
    PresentationMode.showControls();
  },

  mouseDown: function presentationModeMouseDown(evt) {
    var self = PresentationMode;
    if (self.contextMenuOpen) {
      self.contextMenuOpen = false;
      evt.preventDefault();
      return;
    }

    if (evt.button === 0) {
      // Enable clicking of links in presentation mode. Please note:
      // Only links pointing to destinations in the current PDF document work.
      var isInternalLink = (evt.target.href &&
                            evt.target.classList.contains('internalLink'));
      if (!isInternalLink) {
        // Unless an internal link was clicked, advance one page.
        evt.preventDefault();
        PDFViewerApplication.page += (evt.shiftKey ? -1 : 1);
      }
    }
  },

  contextMenu: function presentationModeContextMenu(evt) {
    PresentationMode.contextMenuOpen = true;
  }
};

(function presentationModeClosure() {
  function presentationModeChange(e) {
    if (PresentationMode.isFullscreen) {
      PresentationMode.enter();
    } else {
      PresentationMode.exit();
    }
  }

  window.addEventListener('fullscreenchange', presentationModeChange, false);
  window.addEventListener('mozfullscreenchange', presentationModeChange, false);
  window.addEventListener('webkitfullscreenchange', presentationModeChange,
                          false);
  window.addEventListener('MSFullscreenChange', presentationModeChange, false);
})();
<|endoftext|>