<commit_before>'use strict';

const { exec } = require('child_process');
const path = require('path');
const ApiGateway = require('./ApiGateway');
const ApiGatewayWebSocket = require('./ApiGatewayWebSocket');
const debugLog = require('./debugLog');
const functionHelper = require('./functionHelper');
const { createDefaultApiKey, satisfiesVersionRange } = require('./utils');
const { CUSTOM_OPTION, supportedRuntimes } = require('./config/index.js');
const { peerDependencies } = require('../package.json');

module.exports = class ServerlessOffline {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.service = serverless.service;
    this.serverlessLog = serverless.cli.log.bind(serverless.cli);
    this.options = options;
    this.exitCode = 0;
    this._experimentalWarningNotified = false;

    this.commands = {
      offline: {
        // add start nested options
        commands: {
          start: {
            lifecycleEvents: ['init', 'end'],
            usage:
              'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
          },
        },
        lifecycleEvents: ['start'],
        options: {
          apiKey: {
            usage:
              'Defines the API key value to be used for endpoints marked as private. Defaults to a random hash.',
          },
          binPath: {
            shortcut: 'b',
            usage: 'Path to the Serverless binary.',
          },
          cacheInvalidationRegex: {
            usage:
              'Provide the plugin with a regexp to use for cache invalidation. Default: node_modules',
          },
          corsAllowHeaders: {
            usage:
              'Used to build the Access-Control-Allow-Headers header for CORS support.',
          },
          corsAllowOrigin: {
            usage:
              'Used to build the Access-Control-Allow-Origin header for CORS support.',
          },
          corsDisallowCredentials: {
            usage:
              'Used to override the Access-Control-Allow-Credentials default (which is true) to false.',
          },
          corsExposedHeaders: {
            usage:
              'USed to build the Access-Control-Exposed-Headers response header for CORS support',
          },
          disableCookieValidation: {
            usage: 'Used to disable cookie-validation on hapi.js-server',
          },
          enforceSecureCookies: {
            usage: 'Enforce secure cookies',
          },
          exec: {
            usage:
              'When provided, a shell script is executed when the server starts up, and the server will shut down after handling this command.',
          },
          hideStackTraces: {
            usage: 'Hide the stack trace on lambda failure. Default: false',
          },
          host: {
            shortcut: 'o',
            usage: 'The host name to listen on. Default: localhost',
          },
          httpsProtocol: {
            shortcut: 'H',
            usage:
              'To enable HTTPS, specify directory (relative to your cwd, typically your project dir) for both cert.pem and key.pem files.',
          },
          location: {
            shortcut: 'l',
            usage: "The root location of the handlers' files.",
          },
          noAuth: {
            usage: 'Turns off all authorizers',
          },
          noEnvironment: {
            usage:
              'Turns off loading of your environment variables from serverless.yml. Allows the usage of tools such as PM2 or docker-compose.',
          },
          noTimeout: {
            shortcut: 't',
            usage: 'Disables the timeout feature.',
          },
          port: {
            shortcut: 'P',
            usage: 'Port to listen on. Default: 3000',
          },
          prefix: {
            shortcut: 'p',
            usage:
              'Adds a prefix to every path, to send your requests to http://localhost:3000/prefix/[your_path] instead.',
          },
          preserveTrailingSlash: {
            usage: 'Used to keep trailing slashes on the request path',
          },
          printOutput: {
            usage: 'Outputs your lambda response to the terminal.',
          },
          providedRuntime: {
            usage: 'Sets the provided runtime for lambdas',
          },
          region: {
            shortcut: 'r',
            usage: 'The region used to populate your templates.',
          },
          resourceRoutes: {
            usage:
              'Turns on loading of your HTTP proxy settings from serverless.yml.',
          },
          showDuration: {
            usage: 'Show the execution time duration of the lambda function.',
          },
          skipCacheInvalidation: {
            shortcut: 'c',
            usage:
              'Tells the plugin to skip require cache invalidation. A script reloading tool like Nodemon might then be needed',
          },
          stage: {
            shortcut: 's',
            usage: 'The stage used to populate your templates.',
          },
          useSeparateProcesses: {
            usage: 'Uses separate node processes for handlers',
          },
          websocketPort: {
            usage: 'Websocket port to listen on. Default: 3001',
          },
        },
        usage: 'Simulates API Gateway to call your lambda functions offline.',
      },
    };

    this.hooks = {
      'offline:start:init': this.start.bind(this),
      'offline:start': this.startWithExplicitEnd.bind(this),
      'offline:start:end': this.end.bind(this),
    };
  }

  printBlankLine() {
    console.log();
  }

  logPluginIssue() {
    this.serverlessLog(
      'If you think this is an issue with the plugin please submit it, thanks!',
    );
    this.serverlessLog('https://github.com/dherault/serverless-offline/issues');
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  start() {
    this._verifyServerlessVersionCompatibility();

    // Some users would like to know their environment outside of the handler
    process.env.IS_OFFLINE = true;

    return Promise.resolve(this._buildServer())
      .then(() => this.apiGateway._listen())
      .then(() => this.hasWebsocketRoutes && this.apiGatewayWebSocket._listen())
      .then(() =>
        this.options.exec
          ? this._executeShellScript()
          : this._listenForTermination(),
      );
  }

  /**
   * Entry point for the plugin (sls offline) when running 'sls offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running sls offline that can be expected, but docs say that
   * 'sls offline start' will provide the init and end hooks for other plugins to consume
   * */
  startWithExplicitEnd() {
    return this.start().then(() => this.end());
  }

  _listenForTermination() {
    // SIGINT will be usually sent when user presses ctrl+c
    const waitForSigInt = new Promise((resolve) => {
      process.on('SIGINT', () => resolve('SIGINT'));
    });

    // SIGTERM is a default termination signal in many cases,
    // for example when "killing" a subprocess spawned in node
    // with child_process methods
    const waitForSigTerm = new Promise((resolve) => {
      process.on('SIGTERM', () => resolve('SIGTERM'));
    });

    return Promise.race([waitForSigInt, waitForSigTerm]).then((command) => {
      this.serverlessLog(`Got ${command} signal. Offline Halting...`);
    });
  }

  _executeShellScript() {
    const command = this.options.exec;
    const options = {
      env: {
        IS_OFFLINE: true,
        ...this.service.provider.environment,
        ...this.originalEnvironment,
      },
    };

    this.serverlessLog(`Offline executing script [${command}]`);

    return new Promise((resolve) => {
      exec(command, options, (error, stdout, stderr) => {
        this.serverlessLog(`exec stdout: [${stdout}]`);
        this.serverlessLog(`exec stderr: [${stderr}]`);

        if (error) {
          // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
          this.serverlessLog(`Offline error executing script [${error}]`);
          this.exitCode = error.code || 1;
        }
        resolve();
      });
    });
  }

  _buildServer() {
    // Methods
    this._setOptions(); // Will create meaningful options from cli options
    this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers

    this.apiGateway = new ApiGateway(
      this.serverless,
      this.options,
      this.velocityContextOptions,
    );

    const server = this.apiGateway._createServer();

    this.hasWebsocketRoutes = false;
    this.apiGatewayWebSocket = new ApiGatewayWebSocket(
      this.serverless,
      this.options,
    );
    this.apiGatewayWebSocket._createWebSocket();

    this._setupEvents();
    this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource
    this.apiGateway._create404Route(); // Not found handling

    return server;
  }

  _storeOriginalEnvironment() {
    this.originalEnvironment = { ...process.env };
  }

  _setOptions() {
    // Merge the different sources of values for this.options
    // Precedence is: command line options, YAML options, defaults.
    const defaultOptions = {
      apiKey: createDefaultApiKey(),
      cacheInvalidationRegex: 'node_modules',
      corsAllowOrigin: '*',
      corsAllowCredentials: true,
      corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
      corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
      disableCookieValidation: false,
      enforceSecureCookies: false,
      exec: '',
      hideStackTraces: false,
      host: 'localhost',
      httpsProtocol: '',
      location: '.',
      noAuth: false,
      noEnvironment: false,
      noTimeout: false,
      port: 3000,
      prefix: '/',
      preserveTrailingSlash: false,
      printOutput: false,
      providedRuntime: '',
      showDuration: false,
      stage: this.service.provider.stage,
      region: this.service.provider.region,
      resourceRoutes: false,
      skipCacheInvalidation: false,
      useSeparateProcesses: false,
      websocketPort: 3001,
    };

    // In the constructor, stage and regions are set to undefined
    if (this.options.stage === undefined) delete this.options.stage;
    if (this.options.region === undefined) delete this.options.region;

    const customOptions = (this.service.custom || {})[CUSTOM_OPTION];
    this.options = { ...defaultOptions, ...customOptions, ...this.options };

    // Prefix must start and end with '/'
    if (!this.options.prefix.startsWith('/'))
      this.options.prefix = `/${this.options.prefix}`;
    if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';

    this.velocityContextOptions = {
      stage: this.options.stage,
      stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
    };

    // Parse CORS options
    this.options.corsAllowOrigin = this.options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',');
    this.options.corsAllowHeaders = this.options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',');
    this.options.corsExposedHeaders = this.options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',');

    if (this.options.corsDisallowCredentials)
      this.options.corsAllowCredentials = false;

    this.options.corsConfig = {
      credentials: this.options.corsAllowCredentials,
      exposedHeaders: this.options.corsExposedHeaders,
      headers: this.options.corsAllowHeaders,
      origin: this.options.corsAllowOrigin,
    };

    this.options.cacheInvalidationRegex = new RegExp(
      this.options.cacheInvalidationRegex,
    );

    this.serverlessLog(
      `Starting Offline: ${this.options.stage}/${this.options.region}.`,
    );
    debugLog('options:', this.options);
  }

  end() {
    this.serverlessLog('Halting offline server');
    functionHelper.cleanup();
    this.apiGateway.server
      .stop({ timeout: 5000 })
      .then(() => process.exit(this.exitCode));
  }

  _setupEvents() {
    this._verifySupportedRuntime();

    const defaultContentType = 'application/json';
    const { apiKeys } = this.service.provider;
    const protectedRoutes = [];

    // for simple API Key authentication model
    if (apiKeys) {
      this.serverlessLog(`Key with token: ${this.options.apiKey}`);

      if (this.options.noAuth) {
        this.serverlessLog(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        );
      } else {
        this.serverlessLog('Remember to use x-api-key on the request headers');
      }
    }

    const serviceRuntime = this.service.provider.runtime;

    Object.keys(this.service.functions).forEach((key) => {
      const fun = this.service.getFunction(key);
      const funName = key;
      const servicePath = path.join(
        this.serverless.config.servicePath,
        this.options.location,
      );
      const funOptions = functionHelper.getFunctionOptions(
        fun,
        key,
        servicePath,
        serviceRuntime,
      );

      debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `);
      this.printBlankLine();
      debugLog(funName, 'runtime', serviceRuntime);
      this.serverlessLog(`Routes for ${funName}:`);

      if (!fun.events) {
        fun.events = [];
      }

      // Add proxy for lamda invoke
      fun.events.push({
        http: {
          integration: 'lambda',
          method: 'POST',
          path: `{apiVersion}/functions/${fun.name}/invocations`,
          request: {
            template: {
              // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json'
              'binary/octet-stream': '$input.body',
            },
          },
          response: {
            headers: {
              'Content-Type': 'application/json',
            },
          },
        },
      });

      // Adds a route for each http endpoint
      fun.events.forEach((event) => {
        if (event.websocket) {
          this.hasWebsocketRoutes = true;

          this._experimentalWebSocketSupportWarning();

          this.apiGatewayWebSocket._createWsAction(
            fun,
            funName,
            servicePath,
            funOptions,
            event,
          );

          return;
        }

        if (!event.http) return;

        this.apiGateway._createRoutes(
          event,
          funOptions,
          protectedRoutes,
          funName,
          servicePath,
          serviceRuntime,
          defaultContentType,
          key,
          fun,
        );
      });
    });
  }

  _verifySupportedRuntime() {
    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      if (this.options.providedRuntime) {
        runtime = this.options.providedRuntime;
      } else {
        throw new Error(
          'Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.',
        );
      }
    }

    // print message but keep working (don't error out or exit process)
    if (!supportedRuntimes.has(runtime)) {
      this.printBlankLine();
      this.serverlessLog(`Warning: found unsupported runtime '${runtime}'`);
    }
  }

  // TODO: missing tests
  _verifyServerlessVersionCompatibility() {
    const { version: currentVersion } = this.serverless;
    const { serverless: requiredVersion } = peerDependencies;

    const versionIsSatisfied = satisfiesVersionRange(
      requiredVersion,
      currentVersion,
    );

    if (!versionIsSatisfied) {
      this.serverlessLog(
        `"Serverless-Offline" requires "Serverless" version ${requiredVersion} but found version ${currentVersion}.
         Be aware that functionality might be limited or has serious bugs.
         To avoid any issues update "Serverless" to a later version.
        `,
        'serverless-offline',
        { color: 'red' },
      );
    }
  }

  // TODO: eventually remove WARNING after release has been deemed stable
  _experimentalWebSocketSupportWarning() {
    // notify only once
    if (this._experimentalWarningNotified) {
      return;
    }

    this.serverlessLog(
      `WebSocket support in "Serverless-Offline is experimental.
       For any bugs, missing features or other feedback file an issue at https://github.com/dherault/serverless-offline/issues
      `,
      'serverless-offline',
      { color: 'magenta' },
    );

    this._experimentalWarningNotified = true;
  }
};
<commit_msg>Add braces<commit_after>'use strict';

const { exec } = require('child_process');
const path = require('path');
const ApiGateway = require('./ApiGateway');
const ApiGatewayWebSocket = require('./ApiGatewayWebSocket');
const debugLog = require('./debugLog');
const functionHelper = require('./functionHelper');
const { createDefaultApiKey, satisfiesVersionRange } = require('./utils');
const { CUSTOM_OPTION, supportedRuntimes } = require('./config/index.js');
const { peerDependencies } = require('../package.json');

module.exports = class ServerlessOffline {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.service = serverless.service;
    this.serverlessLog = serverless.cli.log.bind(serverless.cli);
    this.options = options;
    this.exitCode = 0;
    this._experimentalWarningNotified = false;

    this.commands = {
      offline: {
        // add start nested options
        commands: {
          start: {
            lifecycleEvents: ['init', 'end'],
            usage:
              'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
          },
        },
        lifecycleEvents: ['start'],
        options: {
          apiKey: {
            usage:
              'Defines the API key value to be used for endpoints marked as private. Defaults to a random hash.',
          },
          binPath: {
            shortcut: 'b',
            usage: 'Path to the Serverless binary.',
          },
          cacheInvalidationRegex: {
            usage:
              'Provide the plugin with a regexp to use for cache invalidation. Default: node_modules',
          },
          corsAllowHeaders: {
            usage:
              'Used to build the Access-Control-Allow-Headers header for CORS support.',
          },
          corsAllowOrigin: {
            usage:
              'Used to build the Access-Control-Allow-Origin header for CORS support.',
          },
          corsDisallowCredentials: {
            usage:
              'Used to override the Access-Control-Allow-Credentials default (which is true) to false.',
          },
          corsExposedHeaders: {
            usage:
              'USed to build the Access-Control-Exposed-Headers response header for CORS support',
          },
          disableCookieValidation: {
            usage: 'Used to disable cookie-validation on hapi.js-server',
          },
          enforceSecureCookies: {
            usage: 'Enforce secure cookies',
          },
          exec: {
            usage:
              'When provided, a shell script is executed when the server starts up, and the server will shut down after handling this command.',
          },
          hideStackTraces: {
            usage: 'Hide the stack trace on lambda failure. Default: false',
          },
          host: {
            shortcut: 'o',
            usage: 'The host name to listen on. Default: localhost',
          },
          httpsProtocol: {
            shortcut: 'H',
            usage:
              'To enable HTTPS, specify directory (relative to your cwd, typically your project dir) for both cert.pem and key.pem files.',
          },
          location: {
            shortcut: 'l',
            usage: "The root location of the handlers' files.",
          },
          noAuth: {
            usage: 'Turns off all authorizers',
          },
          noEnvironment: {
            usage:
              'Turns off loading of your environment variables from serverless.yml. Allows the usage of tools such as PM2 or docker-compose.',
          },
          noTimeout: {
            shortcut: 't',
            usage: 'Disables the timeout feature.',
          },
          port: {
            shortcut: 'P',
            usage: 'Port to listen on. Default: 3000',
          },
          prefix: {
            shortcut: 'p',
            usage:
              'Adds a prefix to every path, to send your requests to http://localhost:3000/prefix/[your_path] instead.',
          },
          preserveTrailingSlash: {
            usage: 'Used to keep trailing slashes on the request path',
          },
          printOutput: {
            usage: 'Outputs your lambda response to the terminal.',
          },
          providedRuntime: {
            usage: 'Sets the provided runtime for lambdas',
          },
          region: {
            shortcut: 'r',
            usage: 'The region used to populate your templates.',
          },
          resourceRoutes: {
            usage:
              'Turns on loading of your HTTP proxy settings from serverless.yml.',
          },
          showDuration: {
            usage: 'Show the execution time duration of the lambda function.',
          },
          skipCacheInvalidation: {
            shortcut: 'c',
            usage:
              'Tells the plugin to skip require cache invalidation. A script reloading tool like Nodemon might then be needed',
          },
          stage: {
            shortcut: 's',
            usage: 'The stage used to populate your templates.',
          },
          useSeparateProcesses: {
            usage: 'Uses separate node processes for handlers',
          },
          websocketPort: {
            usage: 'Websocket port to listen on. Default: 3001',
          },
        },
        usage: 'Simulates API Gateway to call your lambda functions offline.',
      },
    };

    this.hooks = {
      'offline:start:init': this.start.bind(this),
      'offline:start': this.startWithExplicitEnd.bind(this),
      'offline:start:end': this.end.bind(this),
    };
  }

  printBlankLine() {
    console.log();
  }

  logPluginIssue() {
    this.serverlessLog(
      'If you think this is an issue with the plugin please submit it, thanks!',
    );
    this.serverlessLog('https://github.com/dherault/serverless-offline/issues');
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  start() {
    this._verifyServerlessVersionCompatibility();

    // Some users would like to know their environment outside of the handler
    process.env.IS_OFFLINE = true;

    return Promise.resolve(this._buildServer())
      .then(() => this.apiGateway._listen())
      .then(() => this.hasWebsocketRoutes && this.apiGatewayWebSocket._listen())
      .then(() =>
        this.options.exec
          ? this._executeShellScript()
          : this._listenForTermination(),
      );
  }

  /**
   * Entry point for the plugin (sls offline) when running 'sls offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running sls offline that can be expected, but docs say that
   * 'sls offline start' will provide the init and end hooks for other plugins to consume
   * */
  startWithExplicitEnd() {
    return this.start().then(() => this.end());
  }

  _listenForTermination() {
    // SIGINT will be usually sent when user presses ctrl+c
    const waitForSigInt = new Promise((resolve) => {
      process.on('SIGINT', () => resolve('SIGINT'));
    });

    // SIGTERM is a default termination signal in many cases,
    // for example when "killing" a subprocess spawned in node
    // with child_process methods
    const waitForSigTerm = new Promise((resolve) => {
      process.on('SIGTERM', () => resolve('SIGTERM'));
    });

    return Promise.race([waitForSigInt, waitForSigTerm]).then((command) => {
      this.serverlessLog(`Got ${command} signal. Offline Halting...`);
    });
  }

  _executeShellScript() {
    const command = this.options.exec;
    const options = {
      env: {
        IS_OFFLINE: true,
        ...this.service.provider.environment,
        ...this.originalEnvironment,
      },
    };

    this.serverlessLog(`Offline executing script [${command}]`);

    return new Promise((resolve) => {
      exec(command, options, (error, stdout, stderr) => {
        this.serverlessLog(`exec stdout: [${stdout}]`);
        this.serverlessLog(`exec stderr: [${stderr}]`);

        if (error) {
          // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
          this.serverlessLog(`Offline error executing script [${error}]`);
          this.exitCode = error.code || 1;
        }
        resolve();
      });
    });
  }

  _buildServer() {
    // Methods
    this._setOptions(); // Will create meaningful options from cli options
    this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers

    this.apiGateway = new ApiGateway(
      this.serverless,
      this.options,
      this.velocityContextOptions,
    );

    const server = this.apiGateway._createServer();

    this.hasWebsocketRoutes = false;
    this.apiGatewayWebSocket = new ApiGatewayWebSocket(
      this.serverless,
      this.options,
    );
    this.apiGatewayWebSocket._createWebSocket();

    this._setupEvents();
    this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource
    this.apiGateway._create404Route(); // Not found handling

    return server;
  }

  _storeOriginalEnvironment() {
    this.originalEnvironment = { ...process.env };
  }

  _setOptions() {
    // Merge the different sources of values for this.options
    // Precedence is: command line options, YAML options, defaults.
    const defaultOptions = {
      apiKey: createDefaultApiKey(),
      cacheInvalidationRegex: 'node_modules',
      corsAllowOrigin: '*',
      corsAllowCredentials: true,
      corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
      corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
      disableCookieValidation: false,
      enforceSecureCookies: false,
      exec: '',
      hideStackTraces: false,
      host: 'localhost',
      httpsProtocol: '',
      location: '.',
      noAuth: false,
      noEnvironment: false,
      noTimeout: false,
      port: 3000,
      prefix: '/',
      preserveTrailingSlash: false,
      printOutput: false,
      providedRuntime: '',
      showDuration: false,
      stage: this.service.provider.stage,
      region: this.service.provider.region,
      resourceRoutes: false,
      skipCacheInvalidation: false,
      useSeparateProcesses: false,
      websocketPort: 3001,
    };

    // In the constructor, stage and regions are set to undefined
    if (this.options.stage === undefined) delete this.options.stage;
    if (this.options.region === undefined) delete this.options.region;

    const customOptions = (this.service.custom || {})[CUSTOM_OPTION];
    this.options = { ...defaultOptions, ...customOptions, ...this.options };

    // Prefix must start and end with '/'
    if (!this.options.prefix.startsWith('/')) {
      this.options.prefix = `/${this.options.prefix}`;
    }
    if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';

    this.velocityContextOptions = {
      stage: this.options.stage,
      stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
    };

    // Parse CORS options
    this.options.corsAllowHeaders = this.options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',');
    this.options.corsAllowOrigin = this.options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',');
    this.options.corsExposedHeaders = this.options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',');

    if (this.options.corsDisallowCredentials) {
      this.options.corsAllowCredentials = false;
    }

    this.options.corsConfig = {
      credentials: this.options.corsAllowCredentials,
      exposedHeaders: this.options.corsExposedHeaders,
      headers: this.options.corsAllowHeaders,
      origin: this.options.corsAllowOrigin,
    };

    this.options.cacheInvalidationRegex = new RegExp(
      this.options.cacheInvalidationRegex,
    );

    this.serverlessLog(
      `Starting Offline: ${this.options.stage}/${this.options.region}.`,
    );
    debugLog('options:', this.options);
  }

  end() {
    this.serverlessLog('Halting offline server');
    functionHelper.cleanup();
    this.apiGateway.server
      .stop({ timeout: 5000 })
      .then(() => process.exit(this.exitCode));
  }

  _setupEvents() {
    this._verifySupportedRuntime();

    const defaultContentType = 'application/json';
    const { apiKeys } = this.service.provider;
    const protectedRoutes = [];

    // for simple API Key authentication model
    if (apiKeys) {
      this.serverlessLog(`Key with token: ${this.options.apiKey}`);

      if (this.options.noAuth) {
        this.serverlessLog(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        );
      } else {
        this.serverlessLog('Remember to use x-api-key on the request headers');
      }
    }

    const serviceRuntime = this.service.provider.runtime;

    Object.keys(this.service.functions).forEach((key) => {
      const fun = this.service.getFunction(key);
      const funName = key;
      const servicePath = path.join(
        this.serverless.config.servicePath,
        this.options.location,
      );
      const funOptions = functionHelper.getFunctionOptions(
        fun,
        key,
        servicePath,
        serviceRuntime,
      );

      debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `);
      this.printBlankLine();
      debugLog(funName, 'runtime', serviceRuntime);
      this.serverlessLog(`Routes for ${funName}:`);

      if (!fun.events) {
        fun.events = [];
      }

      // Add proxy for lamda invoke
      fun.events.push({
        http: {
          integration: 'lambda',
          method: 'POST',
          path: `{apiVersion}/functions/${fun.name}/invocations`,
          request: {
            template: {
              // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json'
              'binary/octet-stream': '$input.body',
            },
          },
          response: {
            headers: {
              'Content-Type': 'application/json',
            },
          },
        },
      });

      // Adds a route for each http endpoint
      fun.events.forEach((event) => {
        if (event.websocket) {
          this.hasWebsocketRoutes = true;

          this._experimentalWebSocketSupportWarning();

          this.apiGatewayWebSocket._createWsAction(
            fun,
            funName,
            servicePath,
            funOptions,
            event,
          );

          return;
        }

        if (!event.http) return;

        this.apiGateway._createRoutes(
          event,
          funOptions,
          protectedRoutes,
          funName,
          servicePath,
          serviceRuntime,
          defaultContentType,
          key,
          fun,
        );
      });
    });
  }

  _verifySupportedRuntime() {
    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      if (this.options.providedRuntime) {
        runtime = this.options.providedRuntime;
      } else {
        throw new Error(
          'Runtime "provided" is unsupported. Please add a --providedRuntime CLI option.',
        );
      }
    }

    // print message but keep working (don't error out or exit process)
    if (!supportedRuntimes.has(runtime)) {
      this.printBlankLine();
      this.serverlessLog(`Warning: found unsupported runtime '${runtime}'`);
    }
  }

  // TODO: missing tests
  _verifyServerlessVersionCompatibility() {
    const { version: currentVersion } = this.serverless;
    const { serverless: requiredVersion } = peerDependencies;

    const versionIsSatisfied = satisfiesVersionRange(
      requiredVersion,
      currentVersion,
    );

    if (!versionIsSatisfied) {
      this.serverlessLog(
        `"Serverless-Offline" requires "Serverless" version ${requiredVersion} but found version ${currentVersion}.
         Be aware that functionality might be limited or has serious bugs.
         To avoid any issues update "Serverless" to a later version.
        `,
        'serverless-offline',
        { color: 'red' },
      );
    }
  }

  // TODO: eventually remove WARNING after release has been deemed stable
  _experimentalWebSocketSupportWarning() {
    // notify only once
    if (this._experimentalWarningNotified) {
      return;
    }

    this.serverlessLog(
      `WebSocket support in "Serverless-Offline is experimental.
       For any bugs, missing features or other feedback file an issue at https://github.com/dherault/serverless-offline/issues
      `,
      'serverless-offline',
      { color: 'magenta' },
    );

    this._experimentalWarningNotified = true;
  }
};
<|endoftext|>


<commit_before>/**
 * Created by 王冬 on 2017/7/20.
 * QQ: 20004604
 * weChat: qq20004604
 * 过滤器，将数字转为对应的文字
 * 使用方法：
 * 1、最标准的vue的过滤器的使用方法。
 * 2、示例：{{item.academic | filter_academic}}
 */

export default {
  install: function (Vue, options) {
    // 学历的转换
    Vue.filter('filter_academic', function (item) {
      let template = {
        '0': '无要求',
        '1': '大专',
        '2': '本科',
        '3': '硕士',
        '4': '博士'
      }
      console.log(template[String(item)])
      if (item < 0 || item > 4) {
        return '错误的学历要求'
      } else {
        return template[String(item)]
      }
    })

    // 融资程度转换
    Vue.filter('filter_financing', function (item) {
      let template = {
        '0': '无限制',
        '1': '不需要融资',
        '2': '未融资',
        '3': '天使轮',
        '4': 'A轮',
        '5': 'B轮',
        '6': 'C轮',
        '7': 'D轮及以上',
        '8': '上市'
      }
      console.log(template[String(item)])
      if (item < 0 || item > 8) {
        return '错误的融资程度'
      } else {
        return template[String(item)]
      }
    })

    // 职位类型转换
    Vue.filter('filter_jobType', function (item) {
      let template = {
        '0': '不限',
        '1': '全职',
        '2': '兼职',
        '3': '实习',
        '4': '私活',
        '5': '其他'
      }
      console.log(template[String(item)])
      if (item < 0 || item > 8) {
        return '错误的职位类型'
      } else {
        return template[String(item)]
      }
    })
  }
}
<commit_msg>新增一个对工资格显示格式进行过滤的过滤器<commit_after>/**
 * Created by 王冬 on 2017/7/20.
 * QQ: 20004604
 * weChat: qq20004604
 * 过滤器，将数字转为对应的文字
 * 使用方法：
 * 1、最标准的vue的过滤器的使用方法。
 * 2、示例：{{item.academic | filter_academic}}
 */

export default {
  install: function (Vue, options) {
    // 学历的转换
    Vue.filter('filter_academic', function (item) {
      let template = {
        '0': '无要求',
        '1': '大专',
        '2': '本科',
        '3': '硕士',
        '4': '博士'
      }
      if (item < 0 || item > 4) {
        return '错误的学历要求'
      } else {
        return template[String(item)]
      }
    })

    // 融资程度转换
    Vue.filter('filter_financing', function (item) {
      let template = {
        '0': '无限制',
        '1': '不需要融资',
        '2': '未融资',
        '3': '天使轮',
        '4': 'A轮',
        '5': 'B轮',
        '6': 'C轮',
        '7': 'D轮及以上',
        '8': '上市'
      }
      if (item < 0 || item > 8) {
        return '错误的融资程度'
      } else {
        return template[String(item)]
      }
    })

    // 职位类型转换
    Vue.filter('filter_jobType', function (item) {
      let template = {
        '0': '不限',
        '1': '全职',
        '2': '兼职',
        '3': '实习',
        '4': '私活',
        '5': '其他'
      }
      if (item < 0 || item > 8) {
        return '错误的职位类型'
      } else {
        return template[String(item)]
      }
    })

    // 格式化工资
    Vue.filter('toFixedWage', function (number, afterComma = 0) {
      return Number(number / 1000).toFixed(afterComma) + 'k'
    })
  }
}
<|endoftext|>


<commit_before>import Phaser from 'phaser'

export default class extends Phaser.Plugin {
  init () {
    this.hasPostRender = true
    this.game.input.keyboard.addKey(Phaser.Keyboard.C).onDown.add(this.toggle, this)

    this.setPoints([0])

    // Preallocate ImageData buffer.
    this.post = new ImageData(this.game.width, this.game.height)
  }

  setPoints(points) {
    // Precompute curve shift amounts for each pixel column.
    this.curveOff = new Uint32Array(this.game.width * this.game.height * 4)
    for (let col = 0; col < this.game.width; col++) {
      const bez = this.game.math.bezierInterpolation(points, col / this.game.width)
      const off = Math.floor(bez) * this.game.width * 4
      for (let row = 0; row < this.game.height; row++) {
        const start = row * this.game.width * 4 + col * 4
        this.curveOff[start] = off
        this.curveOff[start + 1] = off
        this.curveOff[start + 2] = off
        this.curveOff[start + 3] = off
      }
    }
  }

  toggle () {
    this.hasPostRender = !this.hasPostRender
  }

  postRender () {
    // Copy ImageData, shifting each column of pixels down by the curve offset.
    const pre = this.game.context.getImageData(0, 0, this.game.width, this.game.height)
    for (let i = 0; i < pre.data.length; i++) {
      this.post.data[i + this.curveOff[i]] = pre.data[i]
    }
    this.game.context.putImageData(this.post, 0, 0)
  }
}
<commit_msg>Move Curve keybinding into setPoints as a hack.<commit_after>import Phaser from 'phaser'

export default class extends Phaser.Plugin {
  init () {
    this.hasPostRender = true

    this.setPoints([0])

    // Preallocate ImageData buffer.
    this.post = new ImageData(this.game.width, this.game.height)
  }

  setPoints(points) {
    this.game.input.keyboard.addKey(Phaser.Keyboard.C).onDown.add(this.toggle, this)

    // Precompute curve shift amounts for each pixel column.
    this.curveOff = new Uint32Array(this.game.width * this.game.height * 4)
    for (let col = 0; col < this.game.width; col++) {
      const bez = this.game.math.bezierInterpolation(points, col / this.game.width)
      const off = Math.floor(bez) * this.game.width * 4
      for (let row = 0; row < this.game.height; row++) {
        const start = row * this.game.width * 4 + col * 4
        this.curveOff[start] = off
        this.curveOff[start + 1] = off
        this.curveOff[start + 2] = off
        this.curveOff[start + 3] = off
      }
    }
  }

  toggle () {
    this.hasPostRender = !this.hasPostRender
  }

  postRender () {
    // Copy ImageData, shifting each column of pixels down by the curve offset.
    const pre = this.game.context.getImageData(0, 0, this.game.width, this.game.height)
    for (let i = 0; i < pre.data.length; i++) {
      this.post.data[i + this.curveOff[i]] = pre.data[i]
    }
    this.game.context.putImageData(this.post, 0, 0)
  }
}
<|endoftext|>


<commit_before>import Phaser from 'phaser'

export default class extends Phaser.Plugin {
  init (points) {
    this.hasPostRender = true

    // Precompute curve shift amounts for each pixel column.
    this.curveOff = new Uint32Array(this.game.width * this.game.height * 4)
    for (let col = 0; col < this.game.width; col++) {
      const bez = this.game.math.bezierInterpolation(points, col / this.game.width)
      const off = Math.floor(bez) * this.game.width * 4
      for (let row = 0; row < this.game.height; row++) {
        const start = row * this.game.width * 4 + col * 4
        this.curveOff[start] = off
        this.curveOff[start + 1] = off
        this.curveOff[start + 2] = off
        this.curveOff[start + 3] = off
      }
    }

    // Preallocate ImageData buffer.
    this.post = new Uint8ClampedArray(this.game.width * this.game.height * 4)
  }

  postRender () {
    // Copy ImageData, shifting each column of pixels down by the curve offset.
    const pre = this.game.context.getImageData(0, 0, this.game.width, this.game.height)
    for (let i = 0; i < pre.data.length; i++) {
      this.post[i] = pre.data[i - this.curveOff[i]]
    }
    this.game.context.putImageData(new ImageData(this.post, pre.width), 0, 0)
  }
}
<commit_msg>Reorder assignment in Curve plugin to avoid cache misses.<commit_after>import Phaser from 'phaser'

export default class extends Phaser.Plugin {
  init (points) {
    this.hasPostRender = true

    // Precompute curve shift amounts for each pixel column.
    this.curveOff = new Uint32Array(this.game.width * this.game.height * 4)
    for (let col = 0; col < this.game.width; col++) {
      const bez = this.game.math.bezierInterpolation(points, col / this.game.width)
      const off = Math.floor(bez) * this.game.width * 4
      for (let row = 0; row < this.game.height; row++) {
        const start = row * this.game.width * 4 + col * 4
        this.curveOff[start] = off
        this.curveOff[start + 1] = off
        this.curveOff[start + 2] = off
        this.curveOff[start + 3] = off
      }
    }

    // Preallocate ImageData buffer.
    this.post = new Uint8ClampedArray(this.game.width * this.game.height * 4)
  }

  postRender () {
    // Copy ImageData, shifting each column of pixels down by the curve offset.
    const pre = this.game.context.getImageData(0, 0, this.game.width, this.game.height)
    for (let i = 0; i < pre.data.length; i++) {
      this.post[i + this.curveOff[i]] = pre.data[i]
    }
    this.game.context.putImageData(new ImageData(this.post, pre.width), 0, 0)
  }
}
<|endoftext|>


<commit_before>import Plugin from './Plugin'
import yo from 'yo-yo'

/**
 * Modal
 *
 */
export default class Modal extends Plugin {
  constructor (core, opts) {
    super(core, opts)
    this.id = 'Modal'
    this.title = 'Modal'
    this.type = 'orchestrator'

    // set default options
    const defaultOptions = {
      target: '.UppyModal',
      defaultTabIcon: yo`
        <svg class="UppyModalTab-icon" width="28" height="28" viewBox="0 0 101 58">
          <path d="M17.582.3L.915 41.713l32.94 13.295L17.582.3zm83.333 41.414L67.975 55.01 84.25.3l16.665 41.414zm-48.998 5.403L63.443 35.59H38.386l11.527 11.526v5.905l-3.063 3.32 1.474 1.36 2.59-2.806 2.59 2.807 1.475-1.357-3.064-3.32v-5.906zm16.06-26.702c-3.973 0-7.194-3.22-7.194-7.193 0-3.973 3.222-7.193 7.193-7.193 3.974 0 7.193 3.22 7.193 7.19 0 3.974-3.22 7.194-7.195 7.194zM70.48 8.682c-.737 0-1.336.6-1.336 1.337 0 .736.6 1.335 1.337 1.335.738 0 1.338-.598 1.338-1.336 0-.74-.6-1.338-1.338-1.338zM33.855 20.415c-3.973 0-7.193-3.22-7.193-7.193 0-3.973 3.22-7.193 7.195-7.193 3.973 0 7.192 3.22 7.192 7.19 0 3.974-3.22 7.194-7.192 7.194zM36.36 8.682c-.737 0-1.336.6-1.336 1.337 0 .736.6 1.335 1.337 1.335.738 0 1.338-.598 1.338-1.336 0-.74-.598-1.338-1.337-1.338z"/>
        </svg>
      `,
      panelSelectorPrefix: 'UppyModalContent-panel'
    }

    // merge default options with the ones set by user
    this.opts = Object.assign({}, defaultOptions, opts)

    this.hideModal = this.hideModal.bind(this)
    this.showModal = this.showModal.bind(this)

    this.addTarget = this.addTarget.bind(this)
    this.showTabPanel = this.showTabPanel.bind(this)
    this.events = this.events.bind(this)
    this.render = this.render.bind(this)
    this.install = this.install.bind(this)
  }

  addTarget (plugin) {
    const callerPluginId = plugin.constructor.name
    const callerPluginName = plugin.title || callerPluginId
    const callerPluginIcon = plugin.icon || this.opts.defaultTabIcon
    const callerPluginType = plugin.type

    if (callerPluginType !== 'acquirer' &&
        callerPluginType !== 'progressindicator' &&
        callerPluginType !== 'presenter') {
      let msg = 'Error: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
      this.core.log(msg)
      return
    }

    const target = {
      id: callerPluginId,
      name: callerPluginName,
      icon: callerPluginIcon,
      type: callerPluginType,
      focus: plugin.focus,
      render: plugin.render,
      isHidden: true
    }

    const modal = this.core.getState().modal

    this.core.setState({
      modal: Object.assign({}, modal, {
        targets: modal.targets.concat([target])
      })
    })

    return this.opts.target
  }

  showTabPanel (id) {
    const modal = this.core.getState().modal

    // hide all panels, except the one that matches current id
    const newTargets = modal.targets.map((target) => {
      if (target.type === 'acquirer') {
        if (target.id === id) {
          target.focus()
          return Object.assign({}, target, {
            isHidden: false
          })
        }
        return Object.assign({}, target, {
          isHidden: true
        })
      }
      return target
    })

    this.core.setState({modal: Object.assign({}, modal, {
      targets: newTargets
    })})
  }

  hideModal () {
    // Straightforward simple way
    // this.core.state.modal.isHidden = true
    // this.core.updateAll()

    // The “right way”
    const modal = this.core.getState().modal

    const newTargets = modal.targets.map((target) => {
      target.isHidden = true
      return target
    })

    this.core.setState({
      modal: Object.assign({}, modal, {
        isHidden: true,
        targets: newTargets
      })
    })

    document.body.classList.remove('is-UppyModal-open')
  }

  showModal () {
    const modal = this.core.getState().modal

    // Show first acquirer plugin when modal is open
    let found = false
    const newTargets = modal.targets.map((target) => {
      if (target.type === 'acquirer' && !found) {
        found = true
        target.focus()

        return Object.assign({}, target, {
          isHidden: false
        })
      }
      return target
    })

    this.core.setState({
      modal: Object.assign({}, modal, {
        isHidden: false,
        targets: newTargets
      })
    })

    // add class to body that sets position fixed
    document.body.classList.add('is-UppyModal-open')
    // focus on modal inner block
    document.querySelector('*[tabindex="0"]').focus()
  }

  events () {
    // Modal open button
    const showModalTrigger = document.querySelector(this.opts.trigger)
    showModalTrigger.addEventListener('click', this.showModal)

    // Close the Modal on esc key press
    document.body.addEventListener('keyup', (event) => {
      if (event.keyCode === 27) {
        this.hideModal()
      }
    })

    // Close on click outside modal or close buttons
    document.addEventListener('click', (e) => {
      if (e.target.classList.contains('js-UppyModal-close')) {
        this.hideModal()
      }
    })
  }

  render (state) {
    // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog

    const modalTargets = state.modal.targets

    const acquirers = modalTargets.filter((target) => {
      return target.type === 'acquirer'
    })

    const progressindicators = modalTargets.filter((target) => {
      return target.type === 'progressindicator'
    })

    const targetClassName = this.opts.target.substring(1)

    return yo`

          ${acquirers.map((target) => {
            return yo`

              <button class="UppyModalTab-btn"
                      role="tab"
                      tabindex="0"
                      aria-controls="${this.opts.panelSelectorPrefix}--${target.id}"
                      aria-selected="${target.isHidden ? 'false' : 'true'}"
                      onclick=${this.showTabPanel.bind(this, target.id)}>
                ${target.icon}
                ${target.name}
              </button>

`
          })}

          ${acquirers.map((target) => {
            return yo`

              ${target.render(state)}

`
          })}

          ${progressindicators.map((target) => {
            return target.render(state)
          })}

        <button class="UppyModal-close"
                title="Close Uppy modal"
                onclick=${this.hideModal}>×</button>

`
  }

  install () {
    // Set default state for Modal
    this.core.setState({modal: {
      isHidden: true,
      targets: []
    }})

    this.el = this.render(this.core.state)
    document.body.appendChild(this.el)

    this.events()
  }
}
<commit_msg>add .Uppy common class to Modal<commit_after>import Plugin from './Plugin'
import yo from 'yo-yo'

/**
 * Modal
 *
 */
export default class Modal extends Plugin {
  constructor (core, opts) {
    super(core, opts)
    this.id = 'Modal'
    this.title = 'Modal'
    this.type = 'orchestrator'

    // set default options
    const defaultOptions = {
      target: '.UppyModal',
      defaultTabIcon: yo`
        <svg class="UppyModalTab-icon" width="28" height="28" viewBox="0 0 101 58">
          <path d="M17.582.3L.915 41.713l32.94 13.295L17.582.3zm83.333 41.414L67.975 55.01 84.25.3l16.665 41.414zm-48.998 5.403L63.443 35.59H38.386l11.527 11.526v5.905l-3.063 3.32 1.474 1.36 2.59-2.806 2.59 2.807 1.475-1.357-3.064-3.32v-5.906zm16.06-26.702c-3.973 0-7.194-3.22-7.194-7.193 0-3.973 3.222-7.193 7.193-7.193 3.974 0 7.193 3.22 7.193 7.19 0 3.974-3.22 7.194-7.195 7.194zM70.48 8.682c-.737 0-1.336.6-1.336 1.337 0 .736.6 1.335 1.337 1.335.738 0 1.338-.598 1.338-1.336 0-.74-.6-1.338-1.338-1.338zM33.855 20.415c-3.973 0-7.193-3.22-7.193-7.193 0-3.973 3.22-7.193 7.195-7.193 3.973 0 7.192 3.22 7.192 7.19 0 3.974-3.22 7.194-7.192 7.194zM36.36 8.682c-.737 0-1.336.6-1.336 1.337 0 .736.6 1.335 1.337 1.335.738 0 1.338-.598 1.338-1.336 0-.74-.598-1.338-1.337-1.338z"/>
        </svg>
      `,
      panelSelectorPrefix: 'UppyModalContent-panel'
    }

    // merge default options with the ones set by user
    this.opts = Object.assign({}, defaultOptions, opts)

    this.hideModal = this.hideModal.bind(this)
    this.showModal = this.showModal.bind(this)

    this.addTarget = this.addTarget.bind(this)
    this.showTabPanel = this.showTabPanel.bind(this)
    this.events = this.events.bind(this)
    this.render = this.render.bind(this)
    this.install = this.install.bind(this)
  }

  addTarget (plugin) {
    const callerPluginId = plugin.constructor.name
    const callerPluginName = plugin.title || callerPluginId
    const callerPluginIcon = plugin.icon || this.opts.defaultTabIcon
    const callerPluginType = plugin.type

    if (callerPluginType !== 'acquirer' &&
        callerPluginType !== 'progressindicator' &&
        callerPluginType !== 'presenter') {
      let msg = 'Error: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
      this.core.log(msg)
      return
    }

    const target = {
      id: callerPluginId,
      name: callerPluginName,
      icon: callerPluginIcon,
      type: callerPluginType,
      focus: plugin.focus,
      render: plugin.render,
      isHidden: true
    }

    const modal = this.core.getState().modal

    this.core.setState({
      modal: Object.assign({}, modal, {
        targets: modal.targets.concat([target])
      })
    })

    return this.opts.target
  }

  showTabPanel (id) {
    const modal = this.core.getState().modal

    // hide all panels, except the one that matches current id
    const newTargets = modal.targets.map((target) => {
      if (target.type === 'acquirer') {
        if (target.id === id) {
          target.focus()
          return Object.assign({}, target, {
            isHidden: false
          })
        }
        return Object.assign({}, target, {
          isHidden: true
        })
      }
      return target
    })

    this.core.setState({modal: Object.assign({}, modal, {
      targets: newTargets
    })})
  }

  hideModal () {
    // Straightforward simple way
    // this.core.state.modal.isHidden = true
    // this.core.updateAll()

    // The “right way”
    const modal = this.core.getState().modal

    const newTargets = modal.targets.map((target) => {
      target.isHidden = true
      return target
    })

    this.core.setState({
      modal: Object.assign({}, modal, {
        isHidden: true,
        targets: newTargets
      })
    })

    document.body.classList.remove('is-UppyModal-open')
  }

  showModal () {
    const modal = this.core.getState().modal

    // Show first acquirer plugin when modal is open
    let found = false
    const newTargets = modal.targets.map((target) => {
      if (target.type === 'acquirer' && !found) {
        found = true
        target.focus()

        return Object.assign({}, target, {
          isHidden: false
        })
      }
      return target
    })

    this.core.setState({
      modal: Object.assign({}, modal, {
        isHidden: false,
        targets: newTargets
      })
    })

    // add class to body that sets position fixed
    document.body.classList.add('is-UppyModal-open')
    // focus on modal inner block
    document.querySelector('*[tabindex="0"]').focus()
  }

  events () {
    // Modal open button
    const showModalTrigger = document.querySelector(this.opts.trigger)
    showModalTrigger.addEventListener('click', this.showModal)

    // Close the Modal on esc key press
    document.body.addEventListener('keyup', (event) => {
      if (event.keyCode === 27) {
        this.hideModal()
      }
    })

    // Close on click outside modal or close buttons
    document.addEventListener('click', (e) => {
      if (e.target.classList.contains('js-UppyModal-close')) {
        this.hideModal()
      }
    })
  }

  render (state) {
    // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog

    const modalTargets = state.modal.targets

    const acquirers = modalTargets.filter((target) => {
      return target.type === 'acquirer'
    })

    const progressindicators = modalTargets.filter((target) => {
      return target.type === 'progressindicator'
    })

    const targetClassName = this.opts.target.substring(1)

    return yo`

          ${acquirers.map((target) => {
            return yo`

              <button class="UppyModalTab-btn"
                      role="tab"
                      tabindex="0"
                      aria-controls="${this.opts.panelSelectorPrefix}--${target.id}"
                      aria-selected="${target.isHidden ? 'false' : 'true'}"
                      onclick=${this.showTabPanel.bind(this, target.id)}>
                ${target.icon}
                ${target.name}
              </button>

`
          })}

          ${acquirers.map((target) => {
            return yo`

              ${target.render(state)}

`
          })}

          ${progressindicators.map((target) => {
            return target.render(state)
          })}

        <button class="UppyModal-close"
                title="Close Uppy modal"
                onclick=${this.hideModal}>×</button>

`
  }

  install () {
    // Set default state for Modal
    this.core.setState({modal: {
      isHidden: true,
      targets: []
    }})

    this.el = this.render(this.core.state)
    document.body.appendChild(this.el)

    this.events()
  }
}
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import UniqueId from 'lodash.uniqueid';
import transitions from '../styles/transitions';
import ClickAwayListener from '../internal/ClickAwayListener';
import SnackbarBody from './SnackbarBody';

function getStyles(props, context, state) {
  const {
    muiTheme: {
      baseTheme: {
        spacing: {
          desktopSubheaderHeight,
        },
      },
      zIndex,
    },
  } = context;

  const {open} = state;

  const styles = {
    root: {
      position: 'fixed',
      left: '50%',
      display: 'flex',
      bottom: 0,
      zIndex: zIndex.snackbar,
      visibility: open ? 'visible' : 'hidden',
      transform: open ?
        'translate(-50%, 0)' :
        `translate(-50%, ${desktopSubheaderHeight}px)`,
      transition: `${transitions.easeOut('400ms', 'transform')}, ${
        transitions.easeOut('400ms', 'visibility')}`,
    },
  };

  return styles;
}

class Snackbar extends Component {
  static propTypes = {
    /**
     * The label for the action on the snackbar.
     */
    action: PropTypes.node,
    /**
     * The number of milliseconds to wait before automatically dismissing.
     * If no value is specified the snackbar will dismiss normally.
     * If a value is provided the snackbar can still be dismissed normally.
     * If a snackbar is dismissed before the timer expires, the timer will be cleared.
     */
    autoHideDuration: PropTypes.number,
    /**
     * Override the inline-styles of the body element.
     */
    bodyStyle: PropTypes.object,
    /**
     * The css class name of the root element.
     */
    className: PropTypes.string,
    /**
     * Override the inline-styles of the content element.
     */
    contentStyle: PropTypes.object,
    /**
     * The message to be displayed.
     *
     * (Note: If the message is an element or array, and the `Snackbar` may re-render while it is still open,
     * ensure that the same object remains as the `message` property if you want to avoid the `Snackbar` hiding and
     * showing again)
     */
    message: PropTypes.node.isRequired,
    /**
     * Fired when the action button is touchtapped.
     *
     * @param {object} event Action button event.
     */
    onActionTouchTap: PropTypes.func,
    /**
     * Fired when the `Snackbar` is requested to be closed by a click outside the `Snackbar`, or after the
     * `autoHideDuration` timer expires.
     *
     * Typically `onRequestClose` is used to set state in the parent component, which is used to control the `Snackbar`
     * `open` prop.
     *
     * The `reason` parameter can optionally be used to control the response to `onRequestClose`,
     * for example ignoring `clickaway`.
     *
     * @param {string} reason Can be:`"timeout"` (`autoHideDuration` expired) or: `"clickaway"`
     */
    onRequestClose: PropTypes.func,
    /**
     * Controls whether the `Snackbar` is opened or not.
     */
    open: PropTypes.bool.isRequired,
    /**
     * Override the inline-styles of the root element.
     */
    style: PropTypes.object,
  };

  static contextTypes = {
    muiTheme: PropTypes.object.isRequired,
  };
  componentWillMount() {
    this.setState({
      open: this.props.open,
      message: this.props.message,
      action: this.props.action,
    });
  }

  componentDidMount() {
    if (this.state.open) {
      this.setAutoHideTimer();
      this.setTransitionTimer();
    }
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.open && nextProps.open &&
        (nextProps.message !== this.props.message || nextProps.action !== this.props.action)) {
      this.setState({
        open: false,
      });

      clearTimeout(this.timerOneAtTheTimeId);
      this.timerOneAtTheTimeId = setTimeout(() => {
        this.setState({
          message: nextProps.message,
          action: nextProps.action,
          open: true,
        });
      }, 400);
    } else {
      const open = nextProps.open;

      this.setState({
        open: open !== null ? open : this.state.open,
        message: nextProps.message,
        action: nextProps.action,
      });
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.open !== this.state.open) {
      if (this.state.open) {
        this.setAutoHideTimer();
        this.setTransitionTimer();
      } else {
        clearTimeout(this.timerAutoHideId);
      }
    }
  }

  componentWillUnmount() {
    clearTimeout(this.timerAutoHideId);
    clearTimeout(this.timerTransitionId);
    clearTimeout(this.timerOneAtTheTimeId);
  }

  contentId = UniqueId('snackbar')

  componentClickAway = () => {
    if (this.timerTransitionId) {
      // If transitioning, don't close the snackbar.
      return;
    }

    if (this.props.open !== null && this.props.onRequestClose) {
      this.props.onRequestClose('clickaway');
    } else {
      this.setState({open: false});
    }
  };

  // Timer that controls delay before snackbar auto hides
  setAutoHideTimer() {
    const autoHideDuration = this.props.autoHideDuration;

    if (autoHideDuration > 0) {
      clearTimeout(this.timerAutoHideId);
      this.timerAutoHideId = setTimeout(() => {
        if (this.props.open !== null && this.props.onRequestClose) {
          this.props.onRequestClose('timeout');
        } else {
          this.setState({open: false});
        }
      }, autoHideDuration);
    }
  }

  // Timer that controls delay before click-away events are captured (based on when animation completes)
  setTransitionTimer() {
    this.timerTransitionId = setTimeout(() => {
      this.timerTransitionId = undefined;
    }, 400);
  }

  render() {
    const {
      autoHideDuration, // eslint-disable-line no-unused-vars
      contentStyle,
      bodyStyle,
      message: messageProp, // eslint-disable-line no-unused-vars
      onRequestClose, // eslint-disable-line no-unused-vars
      onActionTouchTap,
      style,
      ...other
    } = this.props;

    const {
      action,
      message,
      open,
    } = this.state;

    const {prepareStyles} = this.context.muiTheme;
    const styles = getStyles(this.props, this.context, this.state);
    const openAttr = open ? {open} : null;

    return (
      <ClickAwayListener onClickAway={open ? this.componentClickAway : null}>
        <div
          {...other}
          role="dialog"
          aria-live="polite"
          aria-describedby={this.contentId}
          {...openAttr}
          style={prepareStyles(Object.assign(styles.root, style))}
        >
          <SnackbarBody
            action={action}
            contentId={this.contentId}
            contentStyle={contentStyle}
            message={message}
            open={open}
            onActionTouchTap={onActionTouchTap}
            style={bodyStyle}
          />
        </div>
      </ClickAwayListener>
    );
  }
}

export default Snackbar;
<commit_msg>#24 minor tidy up of values across the board, alongside a minimum value<commit_after>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import UniqueId from 'lodash.uniqueid';
import transitions from '../styles/transitions';
import ClickAwayListener from '../internal/ClickAwayListener';
import SnackbarBody from './SnackbarBody';

function getStyles(props, context, state) {
  const {
    muiTheme: {
      baseTheme: {
        spacing: {
          desktopSubheaderHeight,
        },
      },
      zIndex,
    },
  } = context;

  const {open} = state;

  const styles = {
    root: {
      position: 'fixed',
      left: '50%',
      display: 'flex',
      bottom: 0,
      zIndex: zIndex.snackbar,
      visibility: open ? 'visible' : 'hidden',
      transform: open ?
        'translate(-50%, 0)' :
        `translate(-50%, ${desktopSubheaderHeight}px)`,
      transition: `${transitions.easeOut('400ms', 'transform')}, ${
        transitions.easeOut('400ms', 'visibility')}`,
    },
  };

  return styles;
}

class Snackbar extends Component {
  static propTypes = {
    /**
     * The label for the action on the snackbar.
     */
    action: PropTypes.node,
    /**
     * The number of milliseconds to wait before automatically dismissing.
     * If no value is specified the snackbar will dismiss normally.
     * If a value is provided the snackbar can still be dismissed normally.
     * If a snackbar is dismissed before the timer expires, the timer will be cleared.
     */
    autoHideDuration: PropTypes.number,
    /**
     * Override the inline-styles of the body element.
     */
    bodyStyle: PropTypes.object,
    /**
     * The css class name of the root element.
     */
    className: PropTypes.string,
    /**
     * Override the inline-styles of the content element.
     */
    contentStyle: PropTypes.object,
    /**
     * The message to be displayed.
     *
     * (Note: If the message is an element or array, and the `Snackbar` may re-render while it is still open,
     * ensure that the same object remains as the `message` property if you want to avoid the `Snackbar` hiding and
     * showing again)
     */
    message: PropTypes.node.isRequired,
    /**
     * Fired when the action button is touchtapped.
     *
     * @param {object} event Action button event.
     */
    onActionTouchTap: PropTypes.func,
    /**
     * Fired when the `Snackbar` is requested to be closed by a click outside the `Snackbar`, or after the
     * `autoHideDuration` timer expires.
     *
     * Typically `onRequestClose` is used to set state in the parent component, which is used to control the `Snackbar`
     * `open` prop.
     *
     * The `reason` parameter can optionally be used to control the response to `onRequestClose`,
     * for example ignoring `clickaway`.
     *
     * @param {string} reason Can be:`"timeout"` (`autoHideDuration` expired) or: `"clickaway"`
     */
    onRequestClose: PropTypes.func,
    /**
     * Controls whether the `Snackbar` is opened or not.
     */
    open: PropTypes.bool.isRequired,
    /**
     * Override the inline-styles of the root element.
     */
    style: PropTypes.object,
  };

  static contextTypes = {
    muiTheme: PropTypes.object.isRequired,
  };
  componentWillMount() {
    this.setState({
      open: this.props.open,
      message: this.props.message,
      action: this.props.action,
    });
  }

  componentDidMount() {
    if (this.state.open) {
      this.setAutoHideTimer();
      this.setTransitionTimer();
    }
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.open && nextProps.open &&
        (nextProps.message !== this.props.message || nextProps.action !== this.props.action)) {
      this.setState({
        open: false,
      });

      clearTimeout(this.timerOneAtTheTimeId);
      this.timerOneAtTheTimeId = setTimeout(() => {
        this.setState({
          message: nextProps.message,
          action: nextProps.action,
          open: true,
        });
      }, 500);
    } else {
      const open = nextProps.open;

      this.setState({
        open: open !== null ? open : this.state.open,
        message: nextProps.message,
        action: nextProps.action,
      });
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.open !== this.state.open) {
      if (this.state.open) {
        this.setAutoHideTimer();
        this.setTransitionTimer();
      } else {
        clearTimeout(this.timerAutoHideId);
      }
    }
  }

  componentWillUnmount() {
    clearTimeout(this.timerAutoHideId);
    clearTimeout(this.timerTransitionId);
    clearTimeout(this.timerOneAtTheTimeId);
  }

  contentId = UniqueId('snackbar')

  componentClickAway = () => {
    if (this.timerTransitionId) {
      // If transitioning, don't close the snackbar.
      return;
    }

    if (this.props.open !== null && this.props.onRequestClose) {
      this.props.onRequestClose('clickaway');
    } else {
      this.setState({open: false});
    }
  };

  /* Timer that controls delay before snackbar auto hides
  has a minimum length of 10 seconds - for accessibility reasons*/
  setAutoHideTimer() {
    let autoHideDuration = this.props.autoHideDuration;
    if (autoHideDuration < 10000) {
      autoHideDuration = 10000;
    }
    if (autoHideDuration > 0) {
      clearTimeout(this.timerAutoHideId);
      this.timerAutoHideId = setTimeout(() => {
        if (this.props.open !== null && this.props.onRequestClose) {
          this.props.onRequestClose('timeout');
        } else {
          this.setState({open: false});
        }
      }, autoHideDuration);
    }
  }

  // Timer that controls delay before click-away events are captured (based on when animation completes)
  setTransitionTimer() {
    this.timerTransitionId = setTimeout(() => {
      this.timerTransitionId = undefined;
    }, 1000);
  }

  render() {
    const {
      autoHideDuration, // eslint-disable-line no-unused-vars
      contentStyle,
      bodyStyle,
      message: messageProp, // eslint-disable-line no-unused-vars
      onRequestClose, // eslint-disable-line no-unused-vars
      onActionTouchTap,
      style,
      ...other
    } = this.props;

    const {
      action,
      message,
      open,
    } = this.state;

    const {prepareStyles} = this.context.muiTheme;
    const styles = getStyles(this.props, this.context, this.state);
    const openAttr = open ? {open} : null;

    return (
      <ClickAwayListener onClickAway={open ? this.componentClickAway : null}>
        <div
          {...other}
          role="dialog"
          aria-live="polite"
          aria-describedby={this.contentId}
          {...openAttr}
          style={prepareStyles(Object.assign(styles.root, style))}
        >
          <SnackbarBody
            action={action}
            contentId={this.contentId}
            contentStyle={contentStyle}
            message={message}
            open={open}
            onActionTouchTap={onActionTouchTap}
            style={bodyStyle}
          />
        </div>
      </ClickAwayListener>
    );
  }
}

export default Snackbar;
<|endoftext|>


<commit_before>// @flow
import getWindow from './getWindow';

export default function getViewportRect(element: Element) {
  const win = getWindow(element);
  const visualViewport = win.visualViewport || {};

  return {
    width: visualViewport.width || win.innerWidth,
    height: visualViewport.height || win.innerHeight,
    x: visualViewport.offsetLeft || 0,
    y: visualViewport.offsetTop || 0,
  };
}
<commit_msg>fix: ios visual viewport<commit_after>// @flow
import getWindow from './getWindow';

export default function getViewportRect(element: Element) {
  const win = getWindow(element);
  const visualViewport = win.visualViewport;

  let width = win.innerWidth;
  let height = win.innerHeight;
  let x = 0;
  let y = 0;

  if (visualViewport) {
    width = visualViewport.width;
    height = visualViewport.height;

    // iOS has a buggy implementation as expected
    if (!/iPhone|iPod|iPad/.test(navigator.platform)) {
      x = visualViewport.offsetLeft;
      y = visualViewport.offsetTop;
    }
  }

  return { width, height, x, y };
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * 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.
 *
 */

/* global _playground */

define(function (require, exports, module) {
    "use strict";

    var EventEmitter = require("eventEmitter"),
        util = require("../util"),
        Promise = require("bluebird");

    /**
     * The Descriptor object provides helper methods for dealing with the
     * low-level native binding to Photoshop. This object will typically
     * not be used by user-level code.
     *
     * Emits low-level Photoshop events such as "select" with
     * the following parameters:
     *    1. @param {?} info about the event, dependent on event type (Note:
     *           this should become more specific as the native interface is
     *           further defined.)
     *
     * @extends EventEmitter
     * @constructor
     * @private
     */
    var Descriptor = function () {
        EventEmitter.call(this);

        this._psEventHandler = this._psEventHandler.bind(this);
    };
    util.inherits(Descriptor, EventEmitter);

    /**
     * Event handler for events from the native bridge.
     *
     * @private
     * @param {*=} err Error information
     * @param {String} eventID typeID for event type
     * @param {Object} payload serialized ActionDescriptor for the event, dependent on event type
     */
    Descriptor.prototype._psEventHandler = function (err, eventID, payload) {
        if (err) {
            this.emit("error", "Failed to handle Photoshop event: " + err);
            return;
        }

        this.emit("all", eventID, payload);
        this.emit(eventID, payload);
    };

    /**
     * Emit the named event with the given arguments as parameters. Throws if the
     * event is "error" and there are no listeners.
     *
     * @override EventEmitter.prototype.emitEvent
     * @param {string|RegExp} event Name of the event to emit and execute listeners for
     * @param {Array=} args Optional array of arguments to be passed to each listener
     * @return {object} Current instance for chaining
     */
    Descriptor.prototype.emitEvent = function (event, args) {
        if (event === "error") {
            var listeners = this.getListeners(event);

            if (listeners.length === 0) {
                var message,
                    error;

                if (args.length > 0 && typeof args[0] === "string") {
                    message = args.shift();
                } else {
                    message = "Unhandled error event";
                }

                error = new Error(message);
                error.args = args;
                throw error;
            }
        }

        Descriptor.super_.prototype.emitEvent.call(this, event, args);
    };

    /**
     * Wraps certain type of parameters making it easier to call Descriptor.prototype.get
     * For arrays, returns the reference to array recursively mapping everything inside
     * For string values, references to the currently active one
     * For objects, leaves them as is.
     *
     * @private
     * @param {(string|Array.<object>|object)} toWrap object to reference to
     * @return {object} Reference to the toWrap object in a form .get will accept
     */
    var _wrap = function (toWrap) {
        var reference;

        if (Array.isArray(toWrap)) {
            reference = {
                ref: toWrap.map(_wrap).reverse()
            };
        } else if (typeof toWrap === "string") {
            reference = {
                ref: toWrap,
                enum: "ordinal",
                value: "targetEnum"
            };
        } else {
            reference = toWrap;

            // Special case for play objects
            if (reference.hasOwnProperty("null")) {
                reference["null"] = _wrap(reference["null"]);
            }

        }

        return reference;
    };

    /**
     * Constructs a property reference from the given action reference and
     * property name.
     *
     * @private
     * @param {(string|Array.<object>|object)} reference
     * @param {string} property
     * @return {Array.<object>}
     */
    var _makePropertyReference = function (reference, property) {
        var propertyDescriptor = {
                ref: "property",
                property: property
            };

        return Array.isArray(reference) ?
                reference.concat(propertyDescriptor) :
                [reference, propertyDescriptor];
    };

    /**
     * Executes a low-level "get" call using an ActionReference.
     *
     * @param {(string|Array.<(string|Object)>|Object)} reference The reference to retrieve. Can be:
     *     - string of a class name
     *     - Object representation of ActionReference key/value pairs
     *     - An array of a combination of the above, which will get turned into the appropriate ActionReference
     * @return {Promise.<?>} The value of the reference, dependent on reference type
     */
    Descriptor.prototype.get = function (reference) {
        var wrappedReference = _wrap(reference),
            getAsync = Promise.promisify(_playground.ps.descriptor.get,
                _playground.ps.descriptor);

        return getAsync(wrappedReference);
    };

    /**
     * Retrieves a property of a reference
     *
     * @param {String} reference The name of the reference
     * @param {String} property The name of the property
     * @return {Promise.<?>} The value of the property, dependent on reference type
     */
    Descriptor.prototype.getProperty = function (reference, property) {
        var propertyReference = _makePropertyReference(reference, property);

        return this.get(propertyReference)
            .then(function (obj) {
                if (!obj || !obj.hasOwnProperty(property)) {
                    throw new Error("No such property: " + property);
                }

                return obj[property];
            });
    };

    /**
     * Defines an enumeration of three constants that control dialog display
     * while executing action descriptors: DONT_DISPLAY, DISPLAY and SILENT.
     *
     * @const
     * @type {Object.<string: number>}
     */
    Descriptor.prototype.interactionMode = _playground.ps.descriptor.interactionMode;

    /**
     * Executes a low-level "play" call on the specified ActionDescriptor.
     *
     * @param {string} name Name of the ActionDescriptor command
     * @param {Object=} descriptor JS Object representation of ActionDescriptor key/value pairs, defaults to {}
     * @param {Object=} options options, defaults to "silent"
     * @return {Promise.<object>} Resolves when the call is complete (Note: eventually, this will
     *     return the value resulting from the execution of the ActionDescriptor, if any).
     */
    Descriptor.prototype.play = function (name, descriptor, options) {
        descriptor = descriptor || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var playAsync = Promise.promisify(_playground.ps.descriptor.play,
            _playground.ps.descriptor);

        return playAsync(name, descriptor, options);
    };

    /**
     * Executes a low-level "play" call on the PlayObject by unwrapping it
     *
     * @param {PlayObject} playObject Contains command, descriptor and options information
     *
     * @returns {Promise} Resolves to the result of the call
     */
    Descriptor.prototype.playObject = function (playObject) {
        var command = playObject.command,
            descriptor = _wrap(playObject.descriptor),
            options = playObject.options;

        return this.play(command, descriptor, options);
    };

    /**
     * Executes a low-level "batchPlay" call on the specified ActionDescriptors.
     *
     * @param {Array.<{name: string, descriptor: object}>} commands Array of ActionDescriptors to play
     * @param {object=} options Options applied to the execution of each ActionDescriptor individually
     * @param {{continueOnError: boolean=}}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.<object>>} Resolves with the list of ActionDescriptor results, or rejects
     *      with either an adapter error, or a single command error if not continueOnError mode. In
     *      continueOnError mode, always resolve with both the results and errors arrays.
     */
    Descriptor.prototype.batchPlay = function (commands, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var batchPlayAsync = Promise.promisify(_playground.ps.descriptor.batchPlay,
            _playground.ps.descriptor);

        return batchPlayAsync(commands, options, batchOptions)
            .then(function (response) {
                // Never reject in continueOnError mode; the caller must always check the results
                if (batchOptions.continueOnError) {
                    return response;
                }

                var theError;
                response[1].some(function (error) {
                    if (error) {
                        theError = error;
                        return true;
                    }
                });

                if (theError) {
                    // otherwise, throw the first error, because there is only one
                    throw theError;
                }

                // if there are no errors, resolve with just the results
                return response[0];
            });
    };

    /**
     * Executes a low-level "batchPlay" call on the specified PlayObjects.
     *
     * @param {Array.<PlayObject>} objects Array of PlayObjects to play
     * @param {object=} options Options applied to the execution of each PlayObject individually
     * @param {{continueOnError: boolean}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.object>} Resolves with the list of ActionDescriptor results.
     */
    Descriptor.prototype.batchPlayObjects = function (objects, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var commands = objects.map(function (object) {
            return {
                name: object.command,
                descriptor: object.descriptor
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "get" calls using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<object>} references The references to retrieve.
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of results.
     */
    Descriptor.prototype.batchGet = function (references, options, batchOptions) {
        var commands = references.map(function (reference) {
            return {
                name: "get",
                descriptor: {
                    "null": _wrap(reference)
                }
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "getProperty" calls using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<object>} references The references to retrieve.
     * @param {string} property
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of property results.
     */
    Descriptor.prototype.batchGetProperty = function (references, property, options, batchOptions) {
        batchOptions = batchOptions || {};
        var propertyReferences = references.map(function (reference) {
            return _makePropertyReference(reference, property);
        });

        return this.batchGet(propertyReferences, options, batchOptions)
            .then(function (response) {
                if (batchOptions.continueOnError) {
                    return response;
                }

                return response.map(function (result) {
                    if (!result || !result.hasOwnProperty(property)) {
                        throw new Error("No such property: " + property);
                    }

                    return result[property];
                });
            });
    };

    /**
     * @type {Descriptor} The Descriptor singleton
     */
    var descriptor = new Descriptor();

    // bind native Photoshop event handler to our handler function
    _playground.setNotifier(_playground.notifierGroup.PHOTOSHOP, {}, descriptor._psEventHandler);

    module.exports = descriptor;
});
<commit_msg>Add a more general batchGetProperties method, and define batchGetProperty in terms of that<commit_after>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * 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.
 *
 */

/* global _playground */

define(function (require, exports, module) {
    "use strict";

    var EventEmitter = require("eventEmitter"),
        util = require("../util"),
        Promise = require("bluebird");

    /**
     * The Descriptor object provides helper methods for dealing with the
     * low-level native binding to Photoshop. This object will typically
     * not be used by user-level code.
     *
     * Emits low-level Photoshop events such as "select" with
     * the following parameters:
     *    1. @param {?} info about the event, dependent on event type (Note:
     *           this should become more specific as the native interface is
     *           further defined.)
     *
     * @extends EventEmitter
     * @constructor
     * @private
     */
    var Descriptor = function () {
        EventEmitter.call(this);

        this._psEventHandler = this._psEventHandler.bind(this);
    };
    util.inherits(Descriptor, EventEmitter);

    /**
     * Event handler for events from the native bridge.
     *
     * @private
     * @param {*=} err Error information
     * @param {String} eventID typeID for event type
     * @param {Object} payload serialized ActionDescriptor for the event, dependent on event type
     */
    Descriptor.prototype._psEventHandler = function (err, eventID, payload) {
        if (err) {
            this.emit("error", "Failed to handle Photoshop event: " + err);
            return;
        }

        this.emit("all", eventID, payload);
        this.emit(eventID, payload);
    };

    /**
     * Emit the named event with the given arguments as parameters. Throws if the
     * event is "error" and there are no listeners.
     *
     * @override EventEmitter.prototype.emitEvent
     * @param {string|RegExp} event Name of the event to emit and execute listeners for
     * @param {Array=} args Optional array of arguments to be passed to each listener
     * @return {object} Current instance for chaining
     */
    Descriptor.prototype.emitEvent = function (event, args) {
        if (event === "error") {
            var listeners = this.getListeners(event);

            if (listeners.length === 0) {
                var message,
                    error;

                if (args.length > 0 && typeof args[0] === "string") {
                    message = args.shift();
                } else {
                    message = "Unhandled error event";
                }

                error = new Error(message);
                error.args = args;
                throw error;
            }
        }

        Descriptor.super_.prototype.emitEvent.call(this, event, args);
    };

    /**
     * Wraps certain type of parameters making it easier to call Descriptor.prototype.get
     * For arrays, returns the reference to array recursively mapping everything inside
     * For string values, references to the currently active one
     * For objects, leaves them as is.
     *
     * @private
     * @param {(string|Array.<object>|object)} toWrap object to reference to
     * @return {object} Reference to the toWrap object in a form .get will accept
     */
    var _wrap = function (toWrap) {
        var reference;

        if (Array.isArray(toWrap)) {
            reference = {
                ref: toWrap.map(_wrap).reverse()
            };
        } else if (typeof toWrap === "string") {
            reference = {
                ref: toWrap,
                enum: "ordinal",
                value: "targetEnum"
            };
        } else {
            reference = toWrap;

            // Special case for play objects
            if (reference.hasOwnProperty("null")) {
                reference["null"] = _wrap(reference["null"]);
            }

        }

        return reference;
    };

    /**
     * Constructs a property reference from the given action reference and
     * property name.
     *
     * @private
     * @param {(string|Array.<object>|object)} reference
     * @param {string} property
     * @return {Array.<object>}
     */
    var _makePropertyReference = function (reference, property) {
        var propertyDescriptor = {
                ref: "property",
                property: property
            };

        return Array.isArray(reference) ?
                reference.concat(propertyDescriptor) :
                [reference, propertyDescriptor];
    };

    /**
     * Executes a low-level "get" call using an ActionReference.
     *
     * @param {(string|Array.<(string|Object)>|Object)} reference The reference to retrieve. Can be:
     *     - string of a class name
     *     - Object representation of ActionReference key/value pairs
     *     - An array of a combination of the above, which will get turned into the appropriate ActionReference
     * @return {Promise.<?>} The value of the reference, dependent on reference type
     */
    Descriptor.prototype.get = function (reference) {
        var wrappedReference = _wrap(reference),
            getAsync = Promise.promisify(_playground.ps.descriptor.get,
                _playground.ps.descriptor);

        return getAsync(wrappedReference);
    };

    /**
     * Retrieves a property of a reference
     *
     * @param {String} reference The name of the reference
     * @param {String} property The name of the property
     * @return {Promise.<?>} The value of the property, dependent on reference type
     */
    Descriptor.prototype.getProperty = function (reference, property) {
        var propertyReference = _makePropertyReference(reference, property);

        return this.get(propertyReference)
            .then(function (obj) {
                if (!obj || !obj.hasOwnProperty(property)) {
                    throw new Error("No such property: " + property);
                }

                return obj[property];
            });
    };

    /**
     * Defines an enumeration of three constants that control dialog display
     * while executing action descriptors: DONT_DISPLAY, DISPLAY and SILENT.
     *
     * @const
     * @type {Object.<string: number>}
     */
    Descriptor.prototype.interactionMode = _playground.ps.descriptor.interactionMode;

    /**
     * Executes a low-level "play" call on the specified ActionDescriptor.
     *
     * @param {string} name Name of the ActionDescriptor command
     * @param {Object=} descriptor JS Object representation of ActionDescriptor key/value pairs, defaults to {}
     * @param {Object=} options options, defaults to "silent"
     * @return {Promise.<object>} Resolves when the call is complete (Note: eventually, this will
     *     return the value resulting from the execution of the ActionDescriptor, if any).
     */
    Descriptor.prototype.play = function (name, descriptor, options) {
        descriptor = descriptor || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var playAsync = Promise.promisify(_playground.ps.descriptor.play,
            _playground.ps.descriptor);

        return playAsync(name, descriptor, options);
    };

    /**
     * Executes a low-level "play" call on the PlayObject by unwrapping it
     *
     * @param {PlayObject} playObject Contains command, descriptor and options information
     *
     * @returns {Promise} Resolves to the result of the call
     */
    Descriptor.prototype.playObject = function (playObject) {
        var command = playObject.command,
            descriptor = _wrap(playObject.descriptor),
            options = playObject.options;

        return this.play(command, descriptor, options);
    };

    /**
     * Executes a low-level "batchPlay" call on the specified ActionDescriptors.
     *
     * @param {Array.<{name: string, descriptor: object}>} commands Array of ActionDescriptors to play
     * @param {object=} options Options applied to the execution of each ActionDescriptor individually
     * @param {{continueOnError: boolean=}}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.<object>>} Resolves with the list of ActionDescriptor results, or rejects
     *      with either an adapter error, or a single command error if not continueOnError mode. In
     *      continueOnError mode, always resolve with both the results and errors arrays.
     */
    Descriptor.prototype.batchPlay = function (commands, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var batchPlayAsync = Promise.promisify(_playground.ps.descriptor.batchPlay,
            _playground.ps.descriptor);

        return batchPlayAsync(commands, options, batchOptions)
            .then(function (response) {
                // Never reject in continueOnError mode; the caller must always check the results
                if (batchOptions.continueOnError) {
                    return response;
                }

                var theError;
                response[1].some(function (error) {
                    if (error) {
                        theError = error;
                        return true;
                    }
                });

                if (theError) {
                    // otherwise, throw the first error, because there is only one
                    throw theError;
                }

                // if there are no errors, resolve with just the results
                return response[0];
            });
    };

    /**
     * Executes a low-level "batchPlay" call on the specified PlayObjects.
     *
     * @param {Array.<PlayObject>} objects Array of PlayObjects to play
     * @param {object=} options Options applied to the execution of each PlayObject individually
     * @param {{continueOnError: boolean}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.object>} Resolves with the list of ActionDescriptor results.
     */
    Descriptor.prototype.batchPlayObjects = function (objects, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var commands = objects.map(function (object) {
            return {
                name: object.command,
                descriptor: object.descriptor
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "get" calls using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<object>} references The references to retrieve.
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of results.
     */
    Descriptor.prototype.batchGet = function (references, options, batchOptions) {
        var commands = references.map(function (reference) {
            return {
                name: "get",
                descriptor: {
                    "null": _wrap(reference)
                }
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "getProperty" calls using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<{reference: object, property: string}>} refObjs
     *      The references and properties to retrieve.
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of property results.
     */
    Descriptor.prototype.batchGetProperties = function (refObjs, options, batchOptions) {
        batchOptions = batchOptions || {};
        var propertyReferences = refObjs.map(function (refObj) {
            return _makePropertyReference(refObj.reference, refObj.property);
        });

        return this.batchGet(propertyReferences, options, batchOptions)
            .then(function (response) {
                if (batchOptions.continueOnError) {
                    return response;
                }

                return response.map(function (result, index) {
                    var property = refObjs[index].property;
                    if (!result || !result.hasOwnProperty(property)) {
                        throw new Error("No such property: " + property);
                    }

                    return result[property];
                });
            });
    };

    /**
     * Executes a sequence of low-level "getProperty" calls for a single property
     * using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {object} references The references to retrieve
     * @param {string} property The property to retrieve
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of property results.
     */
    Descriptor.prototype.batchGetProperty = function (references, property, options, batchOptions) {
        var refObjs = references.map(function (reference) {
            return {
                reference: reference,
                property: property
            };
        });

        return this.batchGetProperties(refObjs, options, batchOptions);
    };

    /**
     * @type {Descriptor} The Descriptor singleton
     */
    var descriptor = new Descriptor();

    // bind native Photoshop event handler to our handler function
    _playground.setNotifier(_playground.notifierGroup.PHOTOSHOP, {}, descriptor._psEventHandler);

    module.exports = descriptor;
});
<|endoftext|>


<commit_before>var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
    args = Array.prototype.slice.call(arguments, 0);
    return path.join.apply(path, [_root].concat(args));
}
exports.root = root;<commit_msg>Change helper.js<commit_after>var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
    args = Array.prototype.slice.call(arguments, 0);
    return path.join.apply(path, [_root].concat(args));
}
exports.root = root;
<|endoftext|>