<commit_before>const moment = require('moment');
const got = require('got');
const url = require('url');
const delay = require('delay');
const getRegistryUrl = require('registry-auth-token/registry-url');
const registryAuthToken = require('registry-auth-token');
const parse = require('github-url-from-git');
const { isBase64 } = require('validator');

const hostRules = require('../../util/host-rules');
const { maskToken } = require('../../util/mask');
const { getNpmrc } = require('./npmrc');

module.exports = {
  getDependency,
  resetCache,
  resetMemCache,
};

let memcache = {};

function resetMemCache() {
  logger.debug('resetMemCache()');
  memcache = {};
}

function resetCache() {
  resetMemCache();
}

async function getDependency(name, maxRetries = 5) {
  let retries = maxRetries;
  logger.trace(`npm.getDependency(${name})`);

  // This is our datastore cache and is cleared at the end of each repo, i.e. we never requery/revalidate during a "run"
  if (memcache[name]) {
    logger.trace('Returning cached result');
    return JSON.parse(memcache[name]);
  }

  // Now check the persistent cache
  const cacheNamespace = 'datasource-npm';
  const cachedResult = await renovateCache.get(cacheNamespace, name);
  if (cachedResult) {
    return cachedResult;
  }

  const scope = name.split('/')[0];
  let regUrl;
  const npmrc = getNpmrc();
  try {
    regUrl = getRegistryUrl(scope, npmrc);
  } catch (err) {
    regUrl = 'https://registry.npmjs.org';
  }
  const pkgUrl = url.resolve(
    regUrl,
    encodeURIComponent(name).replace(/^%40/, '@')
  );
  const authInfo = registryAuthToken(regUrl, { npmrc });
  const headers = {};

  if (authInfo && authInfo.type && authInfo.token) {
    // istanbul ignore if
    if (npmrc && npmrc.massagedAuth && isBase64(authInfo.token)) {
      logger.debug('Massaging authorization type to Basic');
      authInfo.type = 'Basic';
    }
    headers.authorization = `${authInfo.type} ${authInfo.token}`;
    logger.debug(
      { token: maskToken(authInfo.token), npmName: name },
      'Using auth for npm lookup'
    );
  } else if (process.env.NPM_TOKEN && process.env.NPM_TOKEN !== 'undefined') {
    headers.authorization = `Bearer ${process.env.NPM_TOKEN}`;
  }

  if (
    pkgUrl.startsWith('https://registry.npmjs.org') &&
    !pkgUrl.startsWith('https://registry.npmjs.org/@')
  ) {
    // Delete the authorization header for non-scoped public packages to improve http caching
    // Otherwise, authenticated requests are not cacheable until the registry adds "public" to Cache-Control
    // Ref: https://greenbytes.de/tech/webdav/rfc7234.html#caching.authenticated.responses
    delete headers.authorization;
  }

  // This tells our http layer not to serve responses directly from the cache and instead to revalidate them every time
  headers['Cache-Control'] = 'no-cache';

  try {
    const raw = await got(pkgUrl, {
      json: true,
      retry: {
        retries: (retry, err) => {
          if (retries <= 0) {
            return 0;
          }
          let delayUnit = 1000;
          if (process.env.NODE_ENV === 'test') {
            delayUnit = 1;
          }
          const defaultDelay = (5 * delayUnit) / retries;
          retries -= 1;
          // istanbul ignore if
          if (
            err.code === 'ETIMEDOUT' &&
            err.url &&
            !err.url.startsWith('https://registry.npmjs.org')
          ) {
            logger.info(
              { depName: name, url: err.url },
              'Cannot connect to private npm host - skipping lookup'
            );
            return 0;
          }
          if (err.statusCode === 429) {
            const retryAfter = err.headers['retry-after'] || 30;
            logger.info(
              `npm too many requests. retrying after ${retryAfter} seconds`
            );
            return delayUnit * (retryAfter + 1);
          }
          if (err.statusCode === 408) {
            logger.info({ err }, 'npm registry failure: timeout, retrying');
            return defaultDelay;
          }
          if (err.statusCode >= 500 && err.statusCode < 600) {
            logger.info(
              { err },
              'npm registry failure: internal error, retrying'
            );
            return defaultDelay;
          }
          return 0;
        },
      },
      headers,
    });
    const res = raw.body;
    // eslint-disable-next-line no-underscore-dangle
    const returnedName = res.name ? res.name : res._id || '';
    if (returnedName.toLowerCase() !== name.toLowerCase()) {
      logger.warn(
        { lookupName: name, returnedName: res.name, regUrl },
        'Returned name does not match with requested name'
      );
      return null;
    }
    if (!res.versions || !Object.keys(res.versions).length) {
      // Registry returned a 200 OK but with no versions
      if (retries <= 0) {
        logger.info({ dependency: name }, 'No versions returned');
        return null;
      }
      logger.info('No versions returned, retrying');
      await delay(5000 / retries);
      return getDependency(name, 0);
    }

    const latestVersion = res.versions[res['dist-tags'].latest];
    res.repository = res.repository || latestVersion.repository;
    res.homepage = res.homepage || latestVersion.homepage;

    // Determine repository URL
    let sourceUrl;

    if (res.repository && res.repository.url) {
      const extraBaseUrls = [];
      // istanbul ignore next
      hostRules.hosts({ platform: 'github' }).forEach(host => {
        extraBaseUrls.push(host, `gist.${host}`);
      });
      // Massage www out of github URL
      res.repository.url = res.repository.url.replace(
        'www.github.com',
        'github.com'
      );
      if (res.repository.url.startsWith('https://github.com/')) {
        res.repository.url = res.repository.url
          .split('/')
          .slice(0, 5)
          .join('/');
      }
      sourceUrl = parse(res.repository.url, {
        extraBaseUrls,
      });
    }
    if (res.homepage && res.homepage.includes('://github.com')) {
      delete res.homepage;
    }
    // Simplify response before caching and returning
    const dep = {
      name: res.name,
      homepage: res.homepage,
      latestVersion: res['dist-tags'].latest,
      sourceUrl,
      versions: {},
      'dist-tags': res['dist-tags'],
      'renovate-config': latestVersion['renovate-config'],
    };
    if (res.repository && res.repository.directory) {
      dep.sourceDirectory = res.repository.directory;
    }
    if (latestVersion.deprecated) {
      dep.deprecationMessage = `On registry \`${regUrl}\`, the "latest" version (v${
        dep.latestVersion
      }) of dependency \`${name}\` has the following deprecation notice:\n\n\`${
        latestVersion.deprecated
      }\`\n\nMarking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.`;
      dep.deprecationSource = 'npm';
    }
    dep.releases = Object.keys(res.versions).map(version => {
      const release = {
        version,
        gitRef: res.versions[version].gitHead,
      };
      if (res.time && res.time[version]) {
        release.releaseTimestamp = res.time[version];
        release.canBeUnpublished =
          moment().diff(moment(release.releaseTimestamp), 'days') === 0;
      }
      if (res.versions[version].deprecated) {
        release.isDeprecated = true;
      }
      return release;
    });
    logger.trace({ dep }, 'dep');
    // serialize first before saving
    memcache[name] = JSON.stringify(dep);
    const cacheMinutes = process.env.RENOVATE_CACHE_NPM_MINUTES
      ? parseInt(process.env.RENOVATE_CACHE_NPM_MINUTES, 10)
      : 5;
    if (!name.startsWith('@')) {
      await renovateCache.set(cacheNamespace, name, dep, cacheMinutes);
    }
    return dep;
  } catch (err) {
    if (err.statusCode === 401 || err.statusCode === 403) {
      logger.info(
        {
          pkgUrl,
          authInfoType: authInfo ? authInfo.type : undefined,
          authInfoToken: authInfo ? maskToken(authInfo.token) : undefined,
          err,
          statusCode: err.statusCode,
          depName: name,
        },
        `Dependency lookup failure: unauthorized`
      );
      return null;
    }
    if (err.statusCode === 404 || err.code === 'ENOTFOUND') {
      logger.info({ depName: name }, `Dependency lookup failure: not found`);
      logger.debug({
        err,
        token: authInfo ? maskToken(authInfo.token) : 'none',
      });
      return null;
    }
    if (err.name === 'ParseError') {
      // Registry returned a 200 OK but got failed to parse it
      logger.info({ err }, 'npm registry failure: ParseError, retrying');
      await delay(5000 / retries);
      return getDependency(name, retries - 1);
    }
    logger.warn({ err, regUrl, depName: name }, 'npm registry failure');
    throw new Error('registry-failure');
  }
}
<commit_msg>fix(npm): retry ECONNRESET<commit_after>const moment = require('moment');
const got = require('got');
const url = require('url');
const delay = require('delay');
const getRegistryUrl = require('registry-auth-token/registry-url');
const registryAuthToken = require('registry-auth-token');
const parse = require('github-url-from-git');
const { isBase64 } = require('validator');

const hostRules = require('../../util/host-rules');
const { maskToken } = require('../../util/mask');
const { getNpmrc } = require('./npmrc');

module.exports = {
  getDependency,
  resetCache,
  resetMemCache,
};

let memcache = {};

function resetMemCache() {
  logger.debug('resetMemCache()');
  memcache = {};
}

function resetCache() {
  resetMemCache();
}

async function getDependency(name, maxRetries = 5) {
  let retries = maxRetries;
  logger.trace(`npm.getDependency(${name})`);

  // This is our datastore cache and is cleared at the end of each repo, i.e. we never requery/revalidate during a "run"
  if (memcache[name]) {
    logger.trace('Returning cached result');
    return JSON.parse(memcache[name]);
  }

  // Now check the persistent cache
  const cacheNamespace = 'datasource-npm';
  const cachedResult = await renovateCache.get(cacheNamespace, name);
  if (cachedResult) {
    return cachedResult;
  }

  const scope = name.split('/')[0];
  let regUrl;
  const npmrc = getNpmrc();
  try {
    regUrl = getRegistryUrl(scope, npmrc);
  } catch (err) {
    regUrl = 'https://registry.npmjs.org';
  }
  const pkgUrl = url.resolve(
    regUrl,
    encodeURIComponent(name).replace(/^%40/, '@')
  );
  const authInfo = registryAuthToken(regUrl, { npmrc });
  const headers = {};

  if (authInfo && authInfo.type && authInfo.token) {
    // istanbul ignore if
    if (npmrc && npmrc.massagedAuth && isBase64(authInfo.token)) {
      logger.debug('Massaging authorization type to Basic');
      authInfo.type = 'Basic';
    }
    headers.authorization = `${authInfo.type} ${authInfo.token}`;
    logger.debug(
      { token: maskToken(authInfo.token), npmName: name },
      'Using auth for npm lookup'
    );
  } else if (process.env.NPM_TOKEN && process.env.NPM_TOKEN !== 'undefined') {
    headers.authorization = `Bearer ${process.env.NPM_TOKEN}`;
  }

  if (
    pkgUrl.startsWith('https://registry.npmjs.org') &&
    !pkgUrl.startsWith('https://registry.npmjs.org/@')
  ) {
    // Delete the authorization header for non-scoped public packages to improve http caching
    // Otherwise, authenticated requests are not cacheable until the registry adds "public" to Cache-Control
    // Ref: https://greenbytes.de/tech/webdav/rfc7234.html#caching.authenticated.responses
    delete headers.authorization;
  }

  // This tells our http layer not to serve responses directly from the cache and instead to revalidate them every time
  headers['Cache-Control'] = 'no-cache';

  try {
    const raw = await got(pkgUrl, {
      json: true,
      retry: {
        retries: (retry, err) => {
          if (retries <= 0) {
            return 0;
          }
          let delayUnit = 1000;
          if (process.env.NODE_ENV === 'test') {
            delayUnit = 1;
          }
          const defaultDelay = (5 * delayUnit) / retries;
          retries -= 1;
          // istanbul ignore if
          if (
            err.code === 'ETIMEDOUT' &&
            err.url &&
            !err.url.startsWith('https://registry.npmjs.org')
          ) {
            logger.info(
              { depName: name, url: err.url },
              'Cannot connect to private npm host - skipping lookup'
            );
            return 0;
          }
          if (err.statusCode === 429) {
            const retryAfter = err.headers['retry-after'] || 30;
            logger.info(
              `npm too many requests. retrying after ${retryAfter} seconds`
            );
            return delayUnit * (retryAfter + 1);
          }
          if (err.statusCode === 408) {
            logger.info({ err }, 'npm registry failure: timeout, retrying');
            return defaultDelay;
          }
          if (err.statusCode >= 500 && err.statusCode < 600) {
            logger.info(
              { err },
              'npm registry failure: internal error, retrying'
            );
            return defaultDelay;
          }
          return 0;
        },
      },
      headers,
    });
    const res = raw.body;
    // eslint-disable-next-line no-underscore-dangle
    const returnedName = res.name ? res.name : res._id || '';
    if (returnedName.toLowerCase() !== name.toLowerCase()) {
      logger.warn(
        { lookupName: name, returnedName: res.name, regUrl },
        'Returned name does not match with requested name'
      );
      return null;
    }
    if (!res.versions || !Object.keys(res.versions).length) {
      // Registry returned a 200 OK but with no versions
      if (retries <= 0) {
        logger.info({ dependency: name }, 'No versions returned');
        return null;
      }
      logger.info('No versions returned, retrying');
      await delay(5000 / retries);
      return getDependency(name, 0);
    }

    const latestVersion = res.versions[res['dist-tags'].latest];
    res.repository = res.repository || latestVersion.repository;
    res.homepage = res.homepage || latestVersion.homepage;

    // Determine repository URL
    let sourceUrl;

    if (res.repository && res.repository.url) {
      const extraBaseUrls = [];
      // istanbul ignore next
      hostRules.hosts({ platform: 'github' }).forEach(host => {
        extraBaseUrls.push(host, `gist.${host}`);
      });
      // Massage www out of github URL
      res.repository.url = res.repository.url.replace(
        'www.github.com',
        'github.com'
      );
      if (res.repository.url.startsWith('https://github.com/')) {
        res.repository.url = res.repository.url
          .split('/')
          .slice(0, 5)
          .join('/');
      }
      sourceUrl = parse(res.repository.url, {
        extraBaseUrls,
      });
    }
    if (res.homepage && res.homepage.includes('://github.com')) {
      delete res.homepage;
    }
    // Simplify response before caching and returning
    const dep = {
      name: res.name,
      homepage: res.homepage,
      latestVersion: res['dist-tags'].latest,
      sourceUrl,
      versions: {},
      'dist-tags': res['dist-tags'],
      'renovate-config': latestVersion['renovate-config'],
    };
    if (res.repository && res.repository.directory) {
      dep.sourceDirectory = res.repository.directory;
    }
    if (latestVersion.deprecated) {
      dep.deprecationMessage = `On registry \`${regUrl}\`, the "latest" version (v${
        dep.latestVersion
      }) of dependency \`${name}\` has the following deprecation notice:\n\n\`${
        latestVersion.deprecated
      }\`\n\nMarking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.`;
      dep.deprecationSource = 'npm';
    }
    dep.releases = Object.keys(res.versions).map(version => {
      const release = {
        version,
        gitRef: res.versions[version].gitHead,
      };
      if (res.time && res.time[version]) {
        release.releaseTimestamp = res.time[version];
        release.canBeUnpublished =
          moment().diff(moment(release.releaseTimestamp), 'days') === 0;
      }
      if (res.versions[version].deprecated) {
        release.isDeprecated = true;
      }
      return release;
    });
    logger.trace({ dep }, 'dep');
    // serialize first before saving
    memcache[name] = JSON.stringify(dep);
    const cacheMinutes = process.env.RENOVATE_CACHE_NPM_MINUTES
      ? parseInt(process.env.RENOVATE_CACHE_NPM_MINUTES, 10)
      : 5;
    if (!name.startsWith('@')) {
      await renovateCache.set(cacheNamespace, name, dep, cacheMinutes);
    }
    return dep;
  } catch (err) {
    if (err.statusCode === 401 || err.statusCode === 403) {
      logger.info(
        {
          pkgUrl,
          authInfoType: authInfo ? authInfo.type : undefined,
          authInfoToken: authInfo ? maskToken(authInfo.token) : undefined,
          err,
          statusCode: err.statusCode,
          depName: name,
        },
        `Dependency lookup failure: unauthorized`
      );
      return null;
    }
    if (err.statusCode === 404 || err.code === 'ENOTFOUND') {
      logger.info({ depName: name }, `Dependency lookup failure: not found`);
      logger.debug({
        err,
        token: authInfo ? maskToken(authInfo.token) : 'none',
      });
      return null;
    }
    if (err.name === 'ParseError') {
      // Registry returned a 200 OK but got failed to parse it
      logger.info({ err }, 'npm registry failure: ParseError, retrying');
      await delay(5000 / retries);
      return getDependency(name, retries - 1);
    }
    // istanbul ignore if
    if (err.code === 'ECONNRESET') {
      // Usually a temporary error
      logger.info({ err }, 'npm registry failure: ECONNRESET, retrying');
      await delay(5000 / retries);
      return getDependency(name, retries - 1);
    }
    logger.warn({ err, regUrl, depName: name }, 'npm registry failure');
    throw new Error('registry-failure');
  }
}
<|endoftext|>


<commit_before>/**
 * This widget is used to upload files to a folder. Pass a folder model
 * to its constructor as the parent folder that will be uploaded into.
 * Events:
 *   itemComplete: Triggered each time an individual item is finished uploading.
 *   finished: Triggered when the entire set of items is uploaded.
 */
girder.views.UploadWidget = girder.View.extend({
    events: {
        'submit #g-upload-form': 'startUpload',
        'click .g-resume-upload': function () {
            this.$('.g-upload-error-message').html('');
            this.currentFile.resumeUpload();
        },
        'click .g-restart-upload': function () {
            this.$('.g-upload-error-message').html('');
            this.uploadNextFile();
        },
        'change #g-files': function (e) {
            var files = this.$('#g-files')[0].files;

            if (files.length) {
                this.files = files;
                this.filesChanged();
            }
        },
        'click .g-drop-zone': function (e) {
            this.$('#g-files').click();
        },
        'dragenter .g-drop-zone': function (e) {
            e.stopPropagation();
            e.preventDefault();
            e.originalEvent.dataTransfer.dropEffect = 'copy';
            this.$('.g-drop-zone')
                .addClass('g-dropzone-show')
                .html(' Drop files here');
        },
        'dragleave .g-drop-zone': function (e) {
            e.stopPropagation();
            e.preventDefault();
            this.$('.g-drop-zone')
                .removeClass('g-dropzone-show')
                .html(' Browse or drop files');
        },
        'dragover .g-drop-zone': function (e) {
            e.preventDefault();
        },
        'drop .g-drop-zone': 'filesDropped'
    },

    initialize: function (settings) {
        if (settings.noParent) {
            this.parent = null;
            this.parentType = null;
        } else {
            this.parent = settings.parent || settings.folder;
            this.parentType = settings.parentType || 'folder';
        }
        this.files = [];
        this.totalSize = 0;
        this.title = _.has(settings, 'title') ? settings.title : 'Upload files';
        this.modal = _.has(settings, 'modal') ? settings.modal : true;
        this.overrideStart = settings.overrideStart || false;
    },

    render: function () {
        if (this.modal) {
            this.$el.html(girder.templates.uploadWidget({
                parent: this.parent,
                parentType: this.parentType,
                title: this.title
            }));

            var base = this;
            var dialogid;
            if (this.parentType === 'file') {
                dialogid = this.parent.get('_id');
            }

            this.$el.girderModal(this).on('hidden.bs.modal', function () {
                /* If we are showing the resume option, we have a partial upload
                 * that should be deleted, since the user has no way to get back
                 * to it. */
                if ($('.g-resume-upload').length && base.currentFile) {
                    base.currentFile.abortUpload();
                }
                girder.dialogs.handleClose('upload', undefined, dialogid);
            });

            girder.dialogs.handleOpen('upload', undefined, dialogid);
        } else {
            this.$el.html(girder.templates.uploadWidgetNonModal({
                parent: this.parent,
                parentType: this.parentType,
                title: this.title
            }));
        }
        return this;
    },

    filesDropped: function (e) {
        e.stopPropagation();
        e.preventDefault();
        this.$('.g-drop-zone')
            .removeClass('g-dropzone-show')
            .html(' Browse or drop files');
        this.files = e.originalEvent.dataTransfer.files;
        this.filesChanged();
    },

    filesChanged: function () {
        if (this.files.length === 0) {
            this.$('.g-overall-progress-message').text('No files selected');
            this.$('.g-start-upload').addClass('disabled');
        } else {
            this.totalSize = 0;
            _.each(this.files, function (file) {
                this.totalSize += file.size;
            }, this);

            var msg;

            if (this.files.length > 1) {
                msg = 'Selected ' + this.files.length + ' files';
            } else {
                msg = 'Selected ' + this.files[0].name + '';
            }
            this.$('.g-overall-progress-message').html(' ' +
                msg + '  (' + girder.formatSize(this.totalSize) +
                ') -- Press start button');
            this.$('.g-start-upload').removeClass('disabled');
            this.$('.g-progress-overall,.g-progress-current').addClass('hide');
            this.$('.g-current-progress-message').empty();
            this.$('.g-upload-error-message').empty();
        }

        this.trigger('g:filesChanged', this.files);
    },

    startUpload: function (e) {
        e.preventDefault();

        this.$('.g-drop-zone').addClass('hide');
        this.$('.g-start-upload').addClass('disabled');
        this.$('.g-progress-overall,.g-progress-current').removeClass('hide');
        this.$('.g-upload-error-message').empty();

        this.currentIndex = 0;
        this.overallProgress = 0;
        this.trigger('g:uploadStarted');

        if (!this.overrideStart) {
            this.uploadNextFile();
        }
    },

    /**
     * Initializes the upload of a file by requesting the upload token
     * from the server. If successful, this will call _uploadChunk to send the
     * actual bytes from the file if it is of non-zero length.
     */
    uploadNextFile: function () {
        if (this.currentIndex >= this.files.length) {
            // All files have finished
            if (this.modal) {
                this.$el.modal('hide');
            }
            this.trigger('g:uploadFinished');
            return;
        }

        this.currentFile = this.parentType === 'file' ?
                this.parent : new girder.models.FileModel();

        this.currentFile.on('g:upload.complete', function () {
            this.currentIndex += 1;
            this.uploadNextFile();
        }, this).on('g:upload.chunkSent', function (info) {
            this.overallProgress += info.bytes;
        }, this).on('g:upload.progress', function (info) {
            var currentProgress = info.startByte + info.loaded;

            this.$('.g-progress-current>.progress-bar').css('width',
                Math.ceil(100 * currentProgress / info.total) + '%');
            this.$('.g-progress-overall>.progress-bar').css('width',
                Math.ceil(100 * (this.overallProgress + info.loaded) /
                          this.totalSize) + '%');
            this.$('.g-current-progress-message').html(
                '' + (this.currentIndex + 1) + ' of ' +
                    this.files.length + ' - ' + info.file.name + ': ' +
                    girder.formatSize(currentProgress) + ' / ' +
                    girder.formatSize(info.total)
            );
            this.$('.g-overall-progress-message').html('Overall progress: ' +
                girder.formatSize(this.overallProgress + info.loaded) + ' / ' +
                girder.formatSize(this.totalSize));
        }, this).on('g:upload.error', function (info) {
            var html = info.message + ' ' +
                'Click to resume upload';
            $('.g-upload-error-message').html(html);
        }, this).on('g:upload.errorStarting', function (info) {
            var html = info.message + ' ' +
                'Click to restart upload';
            $('.g-upload-error-message').html(html);
        }, this);

        if (this.parentType === 'file') {
            this.currentFile.updateContents(this.files[this.currentIndex]);
        } else {
            this.currentFile.upload(this.parent, this.files[this.currentIndex]);
        }
    }
});
<commit_msg>Add javascript documentation for the upload widget config options<commit_after>/**
 * This widget is used to upload files to a folder. Pass a folder model
 * to its constructor as the parent folder that will be uploaded into.
 * Events:
 *   itemComplete: Triggered each time an individual item is finished uploading.
 *   finished: Triggered when the entire set of items is uploaded.
 */
girder.views.UploadWidget = girder.View.extend({
    events: {
        'submit #g-upload-form': 'startUpload',
        'click .g-resume-upload': function () {
            this.$('.g-upload-error-message').html('');
            this.currentFile.resumeUpload();
        },
        'click .g-restart-upload': function () {
            this.$('.g-upload-error-message').html('');
            this.uploadNextFile();
        },
        'change #g-files': function (e) {
            var files = this.$('#g-files')[0].files;

            if (files.length) {
                this.files = files;
                this.filesChanged();
            }
        },
        'click .g-drop-zone': function (e) {
            this.$('#g-files').click();
        },
        'dragenter .g-drop-zone': function (e) {
            e.stopPropagation();
            e.preventDefault();
            e.originalEvent.dataTransfer.dropEffect = 'copy';
            this.$('.g-drop-zone')
                .addClass('g-dropzone-show')
                .html(' Drop files here');
        },
        'dragleave .g-drop-zone': function (e) {
            e.stopPropagation();
            e.preventDefault();
            this.$('.g-drop-zone')
                .removeClass('g-dropzone-show')
                .html(' Browse or drop files');
        },
        'dragover .g-drop-zone': function (e) {
            e.preventDefault();
        },
        'drop .g-drop-zone': 'filesDropped'
    },

    /**
     * This widget has several configuration options to control its view and
     * behavior. The following keys can be passed in the settings object:
     *
     * @param [parent] If the parent object is known when instantiating this
     * upload widget, pass the object here.
     * @param [parentType=folder] If the parent type is known when instantiating this
     * upload widget, pass the object here. Otherwise set noParent: true and
     * set it later, prior to starting the upload.
     * @param [noParent=false] If the parent object being uploaded into is not known
     * at the time of widget instantiation, pass noParent: true. Callers must
     * ensure that the parent is set by the time uploadNextFile() actually gets
     * called.
     * @param [title="Upload files"] Title for the widget. This is highly recommended
     * when rendering as a modal dialog. To disable rendering of the title, simply
     * pass a falsy object.
     * @param [modal=true] This widget normally renders as a modal dialog. Pass
     * modal: false to disable the modal behavior and simply render underneath a
     * parent element.
     * @param [overrideStart=false] Some callers will want to hook into the pressing
     * of the start upload button and add their own logic prior to actually sending
     * the files. To do so, set overrideStart: true and bind to the "g:uploadStarted"
     * event of this widget. The caller is then responsible for calling "uploadNextFile()"
     * on the widget when they have completed their actions and are ready to actually
     * send the files.
     *
     * Other events:
     *   - "g:filesChanged": This is triggered any time the user changes the
     *     file selection, either by dropping or browsing and selecting new files.
     *     Handlers will receive a single argument, which is the list of chosen files.
     *   - "g:uploadFinished": When all files have been successfully uploaded,
     *     this event is fired.
     */
    initialize: function (settings) {
        if (settings.noParent) {
            this.parent = null;
            this.parentType = null;
        } else {
            this.parent = settings.parent || settings.folder;
            this.parentType = settings.parentType || 'folder';
        }
        this.files = [];
        this.totalSize = 0;
        this.title = _.has(settings, 'title') ? settings.title : 'Upload files';
        this.modal = _.has(settings, 'modal') ? settings.modal : true;
        this.overrideStart = settings.overrideStart || false;
    },

    render: function () {
        if (this.modal) {
            this.$el.html(girder.templates.uploadWidget({
                parent: this.parent,
                parentType: this.parentType,
                title: this.title
            }));

            var base = this;
            var dialogid;
            if (this.parentType === 'file') {
                dialogid = this.parent.get('_id');
            }

            this.$el.girderModal(this).on('hidden.bs.modal', function () {
                /* If we are showing the resume option, we have a partial upload
                 * that should be deleted, since the user has no way to get back
                 * to it. */
                if ($('.g-resume-upload').length && base.currentFile) {
                    base.currentFile.abortUpload();
                }
                girder.dialogs.handleClose('upload', undefined, dialogid);
            });

            girder.dialogs.handleOpen('upload', undefined, dialogid);
        } else {
            this.$el.html(girder.templates.uploadWidgetNonModal({
                parent: this.parent,
                parentType: this.parentType,
                title: this.title
            }));
        }
        return this;
    },

    filesDropped: function (e) {
        e.stopPropagation();
        e.preventDefault();
        this.$('.g-drop-zone')
            .removeClass('g-dropzone-show')
            .html(' Browse or drop files');
        this.files = e.originalEvent.dataTransfer.files;
        this.filesChanged();
    },

    filesChanged: function () {
        if (this.files.length === 0) {
            this.$('.g-overall-progress-message').text('No files selected');
            this.$('.g-start-upload').addClass('disabled');
        } else {
            this.totalSize = 0;
            _.each(this.files, function (file) {
                this.totalSize += file.size;
            }, this);

            var msg;

            if (this.files.length > 1) {
                msg = 'Selected ' + this.files.length + ' files';
            } else {
                msg = 'Selected ' + this.files[0].name + '';
            }
            this.$('.g-overall-progress-message').html(' ' +
                msg + '  (' + girder.formatSize(this.totalSize) +
                ') -- Press start button');
            this.$('.g-start-upload').removeClass('disabled');
            this.$('.g-progress-overall,.g-progress-current').addClass('hide');
            this.$('.g-current-progress-message').empty();
            this.$('.g-upload-error-message').empty();
        }

        this.trigger('g:filesChanged', this.files);
    },

    startUpload: function (e) {
        e.preventDefault();

        this.$('.g-drop-zone').addClass('hide');
        this.$('.g-start-upload').addClass('disabled');
        this.$('.g-progress-overall,.g-progress-current').removeClass('hide');
        this.$('.g-upload-error-message').empty();

        this.currentIndex = 0;
        this.overallProgress = 0;
        this.trigger('g:uploadStarted');

        if (!this.overrideStart) {
            this.uploadNextFile();
        }
    },

    /**
     * Initializes the upload of a file by requesting the upload token
     * from the server. If successful, this will call _uploadChunk to send the
     * actual bytes from the file if it is of non-zero length.
     */
    uploadNextFile: function () {
        if (this.currentIndex >= this.files.length) {
            // All files have finished
            if (this.modal) {
                this.$el.modal('hide');
            }
            this.trigger('g:uploadFinished');
            return;
        }

        this.currentFile = this.parentType === 'file' ?
                this.parent : new girder.models.FileModel();

        this.currentFile.on('g:upload.complete', function () {
            this.currentIndex += 1;
            this.uploadNextFile();
        }, this).on('g:upload.chunkSent', function (info) {
            this.overallProgress += info.bytes;
        }, this).on('g:upload.progress', function (info) {
            var currentProgress = info.startByte + info.loaded;

            this.$('.g-progress-current>.progress-bar').css('width',
                Math.ceil(100 * currentProgress / info.total) + '%');
            this.$('.g-progress-overall>.progress-bar').css('width',
                Math.ceil(100 * (this.overallProgress + info.loaded) /
                          this.totalSize) + '%');
            this.$('.g-current-progress-message').html(
                '' + (this.currentIndex + 1) + ' of ' +
                    this.files.length + ' - ' + info.file.name + ': ' +
                    girder.formatSize(currentProgress) + ' / ' +
                    girder.formatSize(info.total)
            );
            this.$('.g-overall-progress-message').html('Overall progress: ' +
                girder.formatSize(this.overallProgress + info.loaded) + ' / ' +
                girder.formatSize(this.totalSize));
        }, this).on('g:upload.error', function (info) {
            var html = info.message + ' ' +
                'Click to resume upload';
            $('.g-upload-error-message').html(html);
        }, this).on('g:upload.errorStarting', function (info) {
            var html = info.message + ' ' +
                'Click to restart upload';
            $('.g-upload-error-message').html(html);
        }, this);

        if (this.parentType === 'file') {
            this.currentFile.updateContents(this.files[this.currentIndex]);
        } else {
            this.currentFile.upload(this.parent, this.files[this.currentIndex]);
        }
    }
});
<|endoftext|>


<commit_before>define([
	"nwWindow",
	"ember",
	"utils/mkdirp",
	"utils/download",
	"utils/clearfolder"
], function(
	nwWindow,
	Ember,
	mkdirp,
	download,
	clearfolder
) {

	var PATH  = require( "path" ),
	    OS    = require( "os" ),
	    Notif = window.Notification,
	    get   = Ember.get,
	    set   = Ember.set;

	return Ember.Controller.extend({
		needs: [ "livestreamer" ],

		configBinding  : "metadata.package.config",
		retriesBinding : "config.notification-retries",
		intervalBinding: "config.notification-interval",

		// cache related properties
		cacheDir: function() {
			var dir = get( this, "config.notification-cache-dir" );
			return PATH.resolve( dir.replace( "{os-tmpdir}", OS.tmpdir() ) );
		}.property( "config.notification-cache-dir" ),
		cacheTime: function() {
			var days = get( this, "config.notification-cache-time" );
			return days * 24 * 3600 * 1000;
		}.property( "config.notification-cache-time" ),

		// use the app icon as group icon
		iconGroup: function() {
			return get( this, "config.tray-icon" ).replace( "{res}", 256 );
		}.property( "config.tray-icon" ),

		// controller state
		firstRun: true,
		model   : {},
		tries   : 0,

		_error  : false,
		error   : Ember.computed.and( "_error", "enabled" ),
		_next   : null,
		_running: Ember.computed.notEmpty( "_next" ),
		running : Ember.computed.and( "_running", "enabled" ),

		// automatically start polling once the user is logged in and has notifications enabled
		enabled: Ember.computed.and( "auth.isLoggedIn", "settings.notify_enabled" ),
		enabledObserver: function() {
			if ( get( this, "enabled" ) ) {
				this.start();
			} else {
				this.reset();
			}
		}.observes( "enabled" ).on( "init" ),

		/**
		 * Add a newly followed channel to the channel list cache
		 * so it doesn't pop up a new notification on the next query
		 */
		isFollowingChannelObserver: function() {
			if ( !get( this, "enabled" ) ) { return; }
			/** @type {Object} model */
			var model     = get( this, "model" );
			var following = get( this, "controllers.livestreamer.current._following" );
			var name      = get( following, "channel.name" );
			if ( !following || !name || model.hasOwnProperty( name ) ) { return; }
			model[ name ] = new Date();
		}.observes( "controllers.livestreamer.current._following" ),

		reset: function() {
			Ember.run.cancel( get( this, "_next" ) );

			this.setProperties({
				firstRun: true,
				model   : {},
				tries   : 0,
				_error  : false,
				_next   : null
			});
		},

		start: function() {
			this.reset();

			// collect garbage once at the beginning
			this.gc_icons()
				// then start
				.then( this.check.bind( this ) );
		},

		check: function() {
			if ( !get( this, "enabled" ) ) { return; }

			this.store.find( "twitchStreamsFollowed" )
				.then( this.queryCallback.bind( this ) )
				.then(function( newStreams ) {
					// show notifications
					if ( newStreams && newStreams.length ) {
						return this.prepareNotifications( newStreams );
					}
				}.bind( this ) )
				.then(function() {
					// query again in X milliseconds
					var interval = get( this, "interval" ) || 60000,
					    next     = Ember.run.later( this, this.check, interval );
					set( this, "_next", next );
					set( this, "tries", 0 );
				}.bind( this ) )
				// reset the controller in case of an error
				.catch(function() {
					var tries = get( this, "tries" ),
					    max   = get( this, "retries" );
					if ( ++tries > max ) {
						// we've reached the retry limit
						this.reset();
						set( this, "_error", true );
					} else {
						// immediately retry (with a slight delay)
						var next = Ember.run.later( this, this.check, 1000 );
						set( this, "_next", next );
						set( this, "tries", tries );
					}
				}.bind( this ) );
		},

		queryCallback: function( streams ) {
			/** @type {Object} model */
			var model, newStreams;

			// just fill the cache on the first run
			if ( !get( this, "firstRun" ) ) {
				// get a list of all new streams by comparing the cached streams
				model = get( this, "model" );
				newStreams = streams.filter(function( stream ) {
					var name  = get( stream, "channel.name" ),
					    since = get( stream, "created_at" );
					return name && ( !model.hasOwnProperty( name ) || model[ name ] < since );
				});
			}
			set( this, "firstRun", false );

			// update cache
			model = streams.reduce(function( obj, stream ) {
				obj[ get( stream, "channel.name" ) ] = get( stream, "created_at" );
				return obj;
			}, {} );
			set( this, "model", model );

			return newStreams;
		},

		prepareNotifications: function( streams ) {
			// merge multiple notifications and show a single one
			if ( streams.length > 1 && get( this.settings, "notify_grouping" ) ) {
				return this.showNotificationGroup( streams );

			// show all notifications
			} else {
				// download all channel icons first and save them into a local temp dir...
				return mkdirp( get( this, "cacheDir" ) )
					.then(function( iconTempDir ) {
						return Promise.all( streams.map(function( stream ) {
							var logo = get( stream, "channel.logo" );
							return download( logo, iconTempDir )
								.then(function( file ) {
									// the channel logo is now the local file
									set( stream, "logo", file );
									return stream;
								});
						}) );
					})
					.then(function( streams ) {
						streams.forEach( this.showNotificationSingle, this );
					}.bind( this ) );
			}
		},

		showNotificationGroup: function( streams ) {
			this.showNotification({
				icon : get( this, "groupIcon" ),
				title: "Some of your favorites have started streaming",
				body : streams.map(function( stream ) {
					return get( stream, "channel.display_name" );
				}).join( ", " ),
				click: function() {
					var settings = get( this, "settings.notify_click_group" );
					streams.forEach( this.notificationClick.bind( this, settings ) );
				}.bind( this )
			});
		},

		showNotificationSingle: function( stream ) {
			this.showNotification({
				icon : get( stream, "channel.logo" ),
				title: "%@ has started streaming".fmt( get( stream, "channel.display_name" ) ),
				body : get( stream, "channel.status" ) || "",
				click: function() {
					var settings = get( this, "settings.notify_click" );
					this.notificationClick( settings, stream );
				}.bind( this )
			});
		},

		notificationClick: function( settings, stream ) {
			// always restore the window
			if ( settings !== 0 ) {
				nwWindow.toggleMinimize( true );
				nwWindow.toggleVisibility( true );
			}

			switch( settings ) {
				case 1:
					this.send( "goto", "user.following" );
					break;
				case 2:
					this.send( "openLivestreamer", stream );
					break;
				case 3:
					var url = get( this, "config.twitch-chat-url" )
						.replace( "{channel}", get( stream, "name" ) );
					this.send( "openLivestreamer", stream );
					this.send( "openBrowser", url );
			}
		},

		showNotification: function( obj ) {
			var notify = new Notif( obj.title, {
				icon: obj.icon,
				body: obj.body
			});
			if ( obj.click ) {
				notify.addEventListener( "click", function() {
					this.close();
					obj.click();
				}, false );
			}
		},

		gc_icons: function() {
			var cacheDir  = get( this, "cacheDir" ),
			    cacheTime = get( this, "cacheTime" );

			return clearfolder( cacheDir, cacheTime )
				// always resolve
				.catch(function() {});
		}
	});

});
<commit_msg>Increase query limit of NotificationController<commit_after>define([
	"nwWindow",
	"ember",
	"utils/mkdirp",
	"utils/download",
	"utils/clearfolder"
], function(
	nwWindow,
	Ember,
	mkdirp,
	download,
	clearfolder
) {

	var PATH  = require( "path" ),
	    OS    = require( "os" ),
	    Notif = window.Notification,
	    get   = Ember.get,
	    set   = Ember.set;

	return Ember.Controller.extend({
		needs: [ "livestreamer" ],

		configBinding  : "metadata.package.config",
		retriesBinding : "config.notification-retries",
		intervalBinding: "config.notification-interval",

		// cache related properties
		cacheDir: function() {
			var dir = get( this, "config.notification-cache-dir" );
			return PATH.resolve( dir.replace( "{os-tmpdir}", OS.tmpdir() ) );
		}.property( "config.notification-cache-dir" ),
		cacheTime: function() {
			var days = get( this, "config.notification-cache-time" );
			return days * 24 * 3600 * 1000;
		}.property( "config.notification-cache-time" ),

		// use the app icon as group icon
		iconGroup: function() {
			return get( this, "config.tray-icon" ).replace( "{res}", 256 );
		}.property( "config.tray-icon" ),

		// controller state
		firstRun: true,
		model   : {},
		tries   : 0,

		_error  : false,
		error   : Ember.computed.and( "_error", "enabled" ),
		_next   : null,
		_running: Ember.computed.notEmpty( "_next" ),
		running : Ember.computed.and( "_running", "enabled" ),

		// automatically start polling once the user is logged in and has notifications enabled
		enabled: Ember.computed.and( "auth.isLoggedIn", "settings.notify_enabled" ),
		enabledObserver: function() {
			if ( get( this, "enabled" ) ) {
				this.start();
			} else {
				this.reset();
			}
		}.observes( "enabled" ).on( "init" ),

		/**
		 * Add a newly followed channel to the channel list cache
		 * so it doesn't pop up a new notification on the next query
		 */
		isFollowingChannelObserver: function() {
			if ( !get( this, "enabled" ) ) { return; }
			/** @type {Object} model */
			var model     = get( this, "model" );
			var following = get( this, "controllers.livestreamer.current._following" );
			var name      = get( following, "channel.name" );
			if ( !following || !name || model.hasOwnProperty( name ) ) { return; }
			model[ name ] = new Date();
		}.observes( "controllers.livestreamer.current._following" ),

		reset: function() {
			Ember.run.cancel( get( this, "_next" ) );

			this.setProperties({
				firstRun: true,
				model   : {},
				tries   : 0,
				_error  : false,
				_next   : null
			});
		},

		start: function() {
			this.reset();

			// collect garbage once at the beginning
			this.gc_icons()
				// then start
				.then( this.check.bind( this ) );
		},

		check: function() {
			if ( !get( this, "enabled" ) ) { return; }

			this.store.find( "twitchStreamsFollowed", {
				limit: 100
			})
				.then( this.queryCallback.bind( this ) )
				.then(function( newStreams ) {
					// show notifications
					if ( newStreams && newStreams.length ) {
						return this.prepareNotifications( newStreams );
					}
				}.bind( this ) )
				.then(function() {
					// query again in X milliseconds
					var interval = get( this, "interval" ) || 60000,
					    next     = Ember.run.later( this, this.check, interval );
					set( this, "_next", next );
					set( this, "tries", 0 );
				}.bind( this ) )
				// reset the controller in case of an error
				.catch(function() {
					var tries = get( this, "tries" ),
					    max   = get( this, "retries" );
					if ( ++tries > max ) {
						// we've reached the retry limit
						this.reset();
						set( this, "_error", true );
					} else {
						// immediately retry (with a slight delay)
						var next = Ember.run.later( this, this.check, 1000 );
						set( this, "_next", next );
						set( this, "tries", tries );
					}
				}.bind( this ) );
		},

		queryCallback: function( streams ) {
			/** @type {Object} model */
			var model, newStreams;

			// just fill the cache on the first run
			if ( !get( this, "firstRun" ) ) {
				// get a list of all new streams by comparing the cached streams
				model = get( this, "model" );
				newStreams = streams.filter(function( stream ) {
					var name  = get( stream, "channel.name" ),
					    since = get( stream, "created_at" );
					return name && ( !model.hasOwnProperty( name ) || model[ name ] < since );
				});
			}
			set( this, "firstRun", false );

			// update cache
			model = streams.reduce(function( obj, stream ) {
				obj[ get( stream, "channel.name" ) ] = get( stream, "created_at" );
				return obj;
			}, {} );
			set( this, "model", model );

			return newStreams;
		},

		prepareNotifications: function( streams ) {
			// merge multiple notifications and show a single one
			if ( streams.length > 1 && get( this.settings, "notify_grouping" ) ) {
				return this.showNotificationGroup( streams );

			// show all notifications
			} else {
				// download all channel icons first and save them into a local temp dir...
				return mkdirp( get( this, "cacheDir" ) )
					.then(function( iconTempDir ) {
						return Promise.all( streams.map(function( stream ) {
							var logo = get( stream, "channel.logo" );
							return download( logo, iconTempDir )
								.then(function( file ) {
									// the channel logo is now the local file
									set( stream, "logo", file );
									return stream;
								});
						}) );
					})
					.then(function( streams ) {
						streams.forEach( this.showNotificationSingle, this );
					}.bind( this ) );
			}
		},

		showNotificationGroup: function( streams ) {
			this.showNotification({
				icon : get( this, "groupIcon" ),
				title: "Some of your favorites have started streaming",
				body : streams.map(function( stream ) {
					return get( stream, "channel.display_name" );
				}).join( ", " ),
				click: function() {
					var settings = get( this, "settings.notify_click_group" );
					streams.forEach( this.notificationClick.bind( this, settings ) );
				}.bind( this )
			});
		},

		showNotificationSingle: function( stream ) {
			this.showNotification({
				icon : get( stream, "channel.logo" ),
				title: "%@ has started streaming".fmt( get( stream, "channel.display_name" ) ),
				body : get( stream, "channel.status" ) || "",
				click: function() {
					var settings = get( this, "settings.notify_click" );
					this.notificationClick( settings, stream );
				}.bind( this )
			});
		},

		notificationClick: function( settings, stream ) {
			// always restore the window
			if ( settings !== 0 ) {
				nwWindow.toggleMinimize( true );
				nwWindow.toggleVisibility( true );
			}

			switch( settings ) {
				case 1:
					this.send( "goto", "user.following" );
					break;
				case 2:
					this.send( "openLivestreamer", stream );
					break;
				case 3:
					var url = get( this, "config.twitch-chat-url" )
						.replace( "{channel}", get( stream, "name" ) );
					this.send( "openLivestreamer", stream );
					this.send( "openBrowser", url );
			}
		},

		showNotification: function( obj ) {
			var notify = new Notif( obj.title, {
				icon: obj.icon,
				body: obj.body
			});
			if ( obj.click ) {
				notify.addEventListener( "click", function() {
					this.close();
					obj.click();
				}, false );
			}
		},

		gc_icons: function() {
			var cacheDir  = get( this, "cacheDir" ),
			    cacheTime = get( this, "cacheTime" );

			return clearfolder( cacheDir, cacheTime )
				// always resolve
				.catch(function() {});
		}
	});

});
<|endoftext|>


<commit_before>import React from 'react';
import { IFrame, ContentLoader } from 'olymp-fela';
import { Error404, Page, EditablePage } from './views';

export const EditablePageRoute = (props) => {
  const { Wrapped, flatNavigation, query, pathname, loading } = props;
  const match = flatNavigation.find(item => pathname === item.pathname);
  const { id, binding, pageId, aliasId, bindingId } = match || {};
  const deviceWidth = query['@deviceWidth'];

  if (!match) {
    return (
      <ContentLoader isLoading={loading}>
        <EditablePage
          {...props}
          deviceWidth={deviceWidth}
          render={match =>
            (<IFrame disabled={!deviceWidth}>
              <Wrapped {...props}>
                <Error404 />
              </Wrapped>
            </IFrame>)}
        />
      </ContentLoader>
    );
  }
  return (
    <ContentLoader isLoading={loading}>
      <EditablePage
        {...props}
        deviceWidth={deviceWidth}
        id={pageId || aliasId || id}
        bindingId={bindingId}
        binding={binding}
        render={children =>
          (<IFrame disabled={!deviceWidth}>
            <Wrapped {...props} match={match}>
              {children}
            </Wrapped>
          </IFrame>)}
      />
    </ContentLoader>
  );
};

export const PageRoute = (props) => {
  const { Wrapped, flatNavigation, pathname, loading } = props;
  const match = flatNavigation.find(({ slug }) => pathname === slug);
  const { id, binding, pageId, aliasId, bindingId } = match || {};

  return (
    <Wrapped {...props} match={match}>
      <ContentLoader isLoading={loading}>
        {match
          ? <Page.WithData
            {...props}
            id={pageId || aliasId || id}
            bindingId={bindingId}
            binding={binding}
          />
          : <Error404 />}
      </ContentLoader>
    </Wrapped>
  );
};
<commit_msg>pages metadata<commit_after>import React from 'react';
import { IFrame, ContentLoader } from 'olymp-fela';
import { Error404, Page, EditablePage } from './views';
import { Helmet } from 'olymp';

const renderHelmet = ({ name, description, tags }) => (
  <Helmet
    title={name}
    meta={[
      {
        name: 'description',
        content: description,
      },
      {
        name: 'keywords',
        content: tags ? tags.join(',') : undefined,
      },
    ]}
  />
  );
export const EditablePageRoute = (props) => {
  const { Wrapped, flatNavigation, query, pathname, loading } = props;
  const match = flatNavigation.find(item => pathname === item.pathname);
  const { id, binding, pageId, aliasId, bindingId } = match || {};
  const deviceWidth = query['@deviceWidth'];

  if (!match) {
    return (
      <ContentLoader isLoading={loading}>
        <EditablePage
          {...props}
          deviceWidth={deviceWidth}
          render={match =>
            (<IFrame disabled={!deviceWidth}>
              <Wrapped {...props}>
                {renderHelmet({
                  name: '404',
                  description: 'Seite wurde nicht gefunden',
                })}
                <Error404 />
              </Wrapped>
            </IFrame>)}
        />
      </ContentLoader>
    );
  }
  return (
    <ContentLoader isLoading={loading}>
      <EditablePage
        {...props}
        deviceWidth={deviceWidth}
        id={pageId || aliasId || id}
        bindingId={bindingId}
        binding={binding}
        render={children =>
          (<IFrame disabled={!deviceWidth}>
            <Wrapped {...props} match={match}>
              {renderHelmet(match)}
              {children}
            </Wrapped>
          </IFrame>)}
      />
    </ContentLoader>
  );
};

export const PageRoute = (props) => {
  const { Wrapped, flatNavigation, pathname, loading } = props;
  const match = flatNavigation.find(({ slug }) => pathname === slug);
  const { id, binding, pageId, aliasId, bindingId } = match || {};

  return (
    <Wrapped {...props} match={match}>
      {renderHelmet(match)}
      <ContentLoader isLoading={loading}>
        {match
          ? <Page.WithData
            {...props}
            id={pageId || aliasId || id}
            bindingId={bindingId}
            binding={binding}
          />
          : <Error404 />}
      </ContentLoader>
    </Wrapped>
  );
};
<|endoftext|>


<commit_before>import React from 'react'
import { storiesOf } from '@storybook/react'

storiesOf('Button', module)
  .add('btn', () => (
    <button className='btn'>Button</button>
  ))
  .add('btn-primary', () => (

      <button className='btn btn-primary'>Button</button>
      <button className='btn btn-primary hover'>Button</button>
      <button className='btn btn-primary focus'>Button</button>
      <button className='btn btn-primary selected'>Button</button>
      <button className='btn btn-primary disabled'>Button</button>

  ))
  .add('btn-secondary', () => (

      <button className='btn btn-secondary'>Button</button>
      <button className='btn btn-secondary hover'>Button</button>
      <button className='btn btn-secondary focus'>Button</button>
      <button className='btn btn-secondary selected'>Button</button>
      <button className='btn btn-secondary disabled'>Button</button>

  ))
  .add('btn-danger', () => (

      <button className='btn btn-danger'>Button</button>
      <button className='btn btn-danger hover'>Button</button>
      <button className='btn btn-danger focus'>Button</button>
      <button className='btn btn-danger selected'>Button</button>
      <button className='btn btn-danger disabled'>Button</button>

  ))
  .add('btn-outline', () => (

      <button className='btn btn-outline'>Button</button>
      <button className='btn btn-outline hover'>Button</button>
      <button className='btn btn-outline focus'>Button</button>
      <button className='btn btn-outline selected'>Button</button>
      <button className='btn btn-outline disabled'>Button</button>

  ))
  .add('btn-blue', () => (

      <button className='btn btn-blue'>Button</button>
      <button className='btn btn-blue hover'>Button</button>
      <button className='btn btn-blue focus'>Button</button>
      <button className='btn btn-blue selected'>Button</button>
      <button className='btn btn-blue disabled'>Button</button>

  ))
  .add('btn-purple', () => (

      <button className='btn btn-purple'>Button</button>
      <button className='btn btn-purple hover'>Button</button>
      <button className='btn btn-purple focus'>Button</button>
      <button className='btn btn-purple selected'>Button</button>
      <button className='btn btn-purple disabled'>Button</button>

  ))
<commit_msg>label button examples with their states<commit_after>import React from 'react'
import { storiesOf } from '@storybook/react'

storiesOf('Button', module)
  .add('btn', () => (
    <button className='btn'>Button</button>
  ))
  .add('btn-primary', () => (

      <button className='btn btn-primary'>Button</button>
      <button className='btn btn-primary hover'>hover</button>
      <button className='btn btn-primary focus'>focus</button>
      <button className='btn btn-primary selected'>selected</button>
      <button className='btn btn-primary disabled'>disabled</button>

  ))
  .add('btn-secondary', () => (

      <button className='btn btn-secondary'>Button</button>
      <button className='btn btn-secondary hover'>hover</button>
      <button className='btn btn-secondary focus'>focus</button>
      <button className='btn btn-secondary selected'>selected</button>
      <button className='btn btn-secondary disabled'>disabled</button>

  ))
  .add('btn-danger', () => (

      <button className='btn btn-danger'>Button</button>
      <button className='btn btn-danger hover'>hover</button>
      <button className='btn btn-danger focus'>focus</button>
      <button className='btn btn-danger selected'>selected</button>
      <button className='btn btn-danger disabled'>disabled</button>

  ))
  .add('btn-outline', () => (

      <button className='btn btn-outline'>Button</button>
      <button className='btn btn-outline hover'>hover</button>
      <button className='btn btn-outline focus'>focus</button>
      <button className='btn btn-outline selected'>selected</button>
      <button className='btn btn-outline disabled'>disabled</button>

  ))
  .add('btn-blue', () => (

      <button className='btn btn-blue'>Button</button>
      <button className='btn btn-blue hover'>hover</button>
      <button className='btn btn-blue focus'>focus</button>
      <button className='btn btn-blue selected'>selected</button>
      <button className='btn btn-blue disabled'>disabled</button>

  ))
  .add('btn-purple', () => (

      <button className='btn btn-purple'>Button</button>
      <button className='btn btn-purple hover'>hover</button>
      <button className='btn btn-purple focus'>focus</button>
      <button className='btn btn-purple selected'>selected</button>
      <button className='btn btn-purple disabled'>disabled</button>

  ))
<|endoftext|>


<commit_before>/* eslint-disable global-require */

import {
  configure,
  addDecorator,
} from '@storybook/react';
import { setOptions } from '@storybook/addon-options';
import { withKnobs } from '@storybook/addon-knobs';
import React from 'react';

import 'normalize.css';
import 'mdi/css/materialdesignicons.css';

import Theme from '../src/styles/theme';
import Background from '../src/components/background';

addDecorator((...args) => (
  <Theme>
    <Background
      style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        height: '100vh',
      }}
    >
      {withKnobs(...args)}
    </Background>
  </Theme>
));

setOptions({
  name: 'Materialize React',
  url: 'https://github.com/HenriBeck/materialize-react',
});

configure(() => {
  require('../src/components/ripple/stories');
  require('../src/components/button/stories');
  require('../src/components/spinner/stories');
  require('../src/components/switch/stories');
  require('../src/components/progress/stories');
  require('../src/components/fab/stories');
  require('../src/components/checkbox/stories');
}, module);
<commit_msg>Removed the story of the switch from the storybook config<commit_after>/* eslint-disable global-require */

import {
  configure,
  addDecorator,
} from '@storybook/react';
import { setOptions } from '@storybook/addon-options';
import { withKnobs } from '@storybook/addon-knobs';
import React from 'react';

import 'normalize.css';
import 'mdi/css/materialdesignicons.css';

import Theme from '../src/styles/theme';
import Background from '../src/components/background';

addDecorator((...args) => (
  <Theme>
    <Background
      style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        height: '100vh',
      }}
    >
      {withKnobs(...args)}
    </Background>
  </Theme>
));

setOptions({
  name: 'Materialize React',
  url: 'https://github.com/HenriBeck/materialize-react',
});

configure(() => {
  require('../src/components/ripple/stories');
  require('../src/components/button/stories');
  require('../src/components/spinner/stories');
  require('../src/components/progress/stories');
  require('../src/components/fab/stories');
  require('../src/components/checkbox/stories');
}, module);
<|endoftext|>


<commit_before>(function () {
  'use strict';
  angular.module('civic.add')
    .config(CommunityMainConfig)
    .controller('CommunityMainController', CommunityMainController);

  // @ngInject
  function CommunityMainConfig($stateProvider, $resourceProvider) {
    // TODO: toggle trailing-slash trim after civic-server configured to accept either
    $resourceProvider.defaults.stripTrailingSlashes = false;

    $stateProvider
      .state('community.main', {
        url: '/main',
        templateUrl: 'app/views/community/main/communityMain.tpl.html',
        resolve: {
          Users: 'Users',
          Community: 'Community'
        },
        controller: 'CommunityMainController',
        controllerAs: 'vm',
        data: {
          navMode: 'sub',
          title: 'Community'
        }
      });
  }

  // @ngInject
  function CommunityMainController($scope,
                                   $stateParams,
                                   _,
                                   Security,
                                   Community,
                                   Users) {
    var vm = this;

    vm.isEditor = Security.isEditor();
    vm.isAuthenticated = Security.isAuthenticated();
    vm.leaderboards = {};
    vm.users = [];

    Community.getLeaderboards()
      .then(function(response) {
        angular.copy(response, vm.leaderboards);
      });

    // setup initial paging var and data update
    vm.page = 1;
    vm.count = 24;
    vm.totalItems = Number();
    vm.totalPages = Number();

    $scope.$watch(function(){ return vm.totalItems; }, function() {
      vm.totalPages = Math.ceil(vm.totalItems / vm.count);
    });

    vm.model = {};

    vm.filterFields = [
      {
        key: 'filter',
        type: 'input',
        templateOptions: {
          label: 'Find User',
          required: false
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            updateData();
          }
        }
      }
    ];
    vm.limitFields = [
      {
        key: 'limit',
        type: 'select',
        defaultValue: 'all_time',
        templateOptions: {
          label: 'Limit To',
          required: false,
          options: [
            // this_week, this_month, this_year, all_time
            {name: 'All time', value: 'all_time'},
            {name: 'This week', value: 'this_week'},
            {name: 'This month', value: 'this_month'},
            {name: 'This year', value: 'this_year'}
          ]
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            updateData();
          }
        }
      }
    ];

    vm.sortFields = [
      {
        key: 'sort_by',
        type: 'select',
        defaultValue: 'last_seen',
        templateOptions: {
          label: 'Sort By',
          required: false,
          options: [
            // last_seen, recent_activity, join_date, most_active
            {name: 'Display name', value: 'display_name', sort_order: 'asc'},
            {name: 'Last seen', value: 'last_seen', sort_order: 'asc'},
            {name: 'Recent activity', value: 'recent_activity', sort_order: 'desc'},
            {name: 'Join date', value: 'join_date', sort_order: 'desc'},
            {name: 'Most active', value: 'most_active', sort_order: 'desc'}
          ]
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            if(!_.isUndefined(newValue)) {
              vm.model.sort_order = _.find(field.templateOptions.options, {value: newValue}).sort_order;
            }
            updateData();
          }
        }
      }
    ];

    vm.sortOrderFields = [
      {
        key: 'sort_order',
        type: 'select',
        defaultValue: 'desc',
        templateOptions: {
          label: 'Sort Order',
          required: false,
          options: [
            {name: 'Ascending', value: 'asc'},
            {name: 'Descending', value: 'desc'}
          ]
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            updateData();
          }
        }
      }
    ];

    updateData();

    vm.pageChanged = function() {
      updateData();
    };

    function updateData() {
      var filters = [{
        field: 'display_name',
        term: vm.model.filter
      }];

      var sorting = [{
        field: vm.model.sort_by,
        direction: vm.model.sort_order
      }];
      fetchUsers(vm.count, vm.page, sorting, filters)
        .then(function(data){
          angular.copy(data.result, vm.users);
          vm.totalItems = data.total;
        });
    }

    function fetchUsers(count, page, sorting, filters) {
      var request;

      request= {
        count: count,
        page: page
      };

      if (filters.length > 0) {
        _.each(filters, function(filter) {
          request['filter[' + filter.field + ']'] = filter.term;
        });
      }

      if (sorting.length > 0) {
        _.each(sorting, function(sort) {
          request['sorting[' + sort.field + ']'] = sort.direction;
        });
      }
      return Users.query(request);
    }
  }

})();
<commit_msg>default last_seen sort to desc<commit_after>(function () {
  'use strict';
  angular.module('civic.add')
    .config(CommunityMainConfig)
    .controller('CommunityMainController', CommunityMainController);

  // @ngInject
  function CommunityMainConfig($stateProvider, $resourceProvider) {
    // TODO: toggle trailing-slash trim after civic-server configured to accept either
    $resourceProvider.defaults.stripTrailingSlashes = false;

    $stateProvider
      .state('community.main', {
        url: '/main',
        templateUrl: 'app/views/community/main/communityMain.tpl.html',
        resolve: {
          Users: 'Users',
          Community: 'Community'
        },
        controller: 'CommunityMainController',
        controllerAs: 'vm',
        data: {
          navMode: 'sub',
          title: 'Community'
        }
      });
  }

  // @ngInject
  function CommunityMainController($scope,
                                   $stateParams,
                                   _,
                                   Security,
                                   Community,
                                   Users) {
    var vm = this;

    vm.isEditor = Security.isEditor();
    vm.isAuthenticated = Security.isAuthenticated();
    vm.leaderboards = {};
    vm.users = [];

    Community.getLeaderboards()
      .then(function(response) {
        angular.copy(response, vm.leaderboards);
      });

    // setup initial paging var and data update
    vm.page = 1;
    vm.count = 24;
    vm.totalItems = Number();
    vm.totalPages = Number();

    $scope.$watch(function(){ return vm.totalItems; }, function() {
      vm.totalPages = Math.ceil(vm.totalItems / vm.count);
    });

    vm.model = {};

    vm.filterFields = [
      {
        key: 'filter',
        type: 'input',
        templateOptions: {
          label: 'Find User',
          required: false
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            updateData();
          }
        }
      }
    ];
    vm.limitFields = [
      {
        key: 'limit',
        type: 'select',
        defaultValue: 'all_time',
        templateOptions: {
          label: 'Limit To',
          required: false,
          options: [
            // this_week, this_month, this_year, all_time
            {name: 'All time', value: 'all_time'},
            {name: 'This week', value: 'this_week'},
            {name: 'This month', value: 'this_month'},
            {name: 'This year', value: 'this_year'}
          ]
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            updateData();
          }
        }
      }
    ];

    vm.sortFields = [
      {
        key: 'sort_by',
        type: 'select',
        defaultValue: 'last_seen',
        templateOptions: {
          label: 'Sort By',
          required: false,
          options: [
            // last_seen, recent_activity, join_date, most_active
            {name: 'Display name', value: 'display_name', sort_order: 'asc'},
            {name: 'Last seen', value: 'last_seen', sort_order: 'desc'},
            {name: 'Recent activity', value: 'recent_activity', sort_order: 'desc'},
            {name: 'Join date', value: 'join_date', sort_order: 'desc'},
            {name: 'Most active', value: 'most_active', sort_order: 'desc'}
          ]
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            if(!_.isUndefined(newValue)) {
              vm.model.sort_order = _.find(field.templateOptions.options, {value: newValue}).sort_order;
            }
            updateData();
          }
        }
      }
    ];

    vm.sortOrderFields = [
      {
        key: 'sort_order',
        type: 'select',
        defaultValue: 'desc',
        templateOptions: {
          label: 'Sort Order',
          required: false,
          options: [
            {name: 'Ascending', value: 'asc'},
            {name: 'Descending', value: 'desc'}
          ]
        },
        watcher: {
          listener: function(field, newValue, oldValue, scope, stopWatching) {
            updateData();
          }
        }
      }
    ];

    updateData();

    vm.pageChanged = function() {
      updateData();
    };

    function updateData() {
      var filters = [{
        field: 'display_name',
        term: vm.model.filter
      }];

      var sorting = [{
        field: vm.model.sort_by,
        direction: vm.model.sort_order
      }];
      fetchUsers(vm.count, vm.page, sorting, filters)
        .then(function(data){
          angular.copy(data.result, vm.users);
          vm.totalItems = data.total;
        });
    }

    function fetchUsers(count, page, sorting, filters) {
      var request;

      request= {
        count: count,
        page: page
      };

      if (filters.length > 0) {
        _.each(filters, function(filter) {
          request['filter[' + filter.field + ']'] = filter.term;
        });
      }

      if (sorting.length > 0) {
        _.each(sorting, function(sort) {
          request['sorting[' + sort.field + ']'] = sort.direction;
        });
      }
      return Users.query(request);
    }
  }

})();
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2012, Peter Michaux, http://peter.michaux.ca/
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

/**

@property evento.EventTarget

@description

A constructor function for creating event target objects.

var et = new evento.EventTarget();

The methods of an event target object are inspired by the DOM2 standard.

*/
evento.EventTarget = function() {};

(function() {

    function hasOwnProperty(o, p) {
        return Object.prototype.hasOwnProperty.call(o, p);
    }

    var create = (function() {
        function F() {}
        return function(o) {
            F.prototype = o;
            return new F();
        };
    }());

/**

@property evento.EventTarget.prototype.addEventListener

@parameter type {string} The name of the event.

@parameter listener {object|function} The listener object or callback function.

@description

If the listener is an object then when a matching event type is dispatched on
the event target, the listener object's handleEvent method will be called.

If the listener is a function then when a matching event type is dispatched on
the event target, the listener function is called with event target object set as
the "this" object.

One listener (or type/listener pair to be more precise) can be added only once.

et.addEventListener('change', {handleEvent:function(){}});
et.addEventListener('change', function(){});

*/
    evento.EventTarget.prototype.addEventListener = function(type, listener) {
        hasOwnProperty(this, '_evento_listeners') || (this._evento_listeners = {});
        hasOwnProperty(this._evento_listeners, type) || (this._evento_listeners[type] = []);
        var listeners = this._evento_listeners[type];
        for (var i = 0, ilen = listeners.length; i < ilen; i++) {
            if (listeners[i] === listener) {
                // can only add a listener once
                return;
            }
        }
        listeners.push(listener);
    };

/**

@property evento.EventTarget.prototype.removeEventListener

@parameter type {string} The name of the event.

@parameter listener {object|function} The listener object or callback function.

@description

Removes added listener matching the type/listener combination exactly.
If this combination is not found there are no errors.

var o = {handleEvent:function(){}};
et.removeEventListener('change', o);
et.removeEventListener('change', fn);

*/
    evento.EventTarget.prototype.removeEventListener = function(type, listener) {
        if (hasOwnProperty(this, '_evento_listeners') &&
            hasOwnProperty(this._evento_listeners, type)) {
            var listeners = this._evento_listeners[type];
            for (var i = 0, ilen = listeners.length; i < ilen; i++) {
                if (listeners[i] === listener) {
                    listeners.splice(i, 1);
                    // no need to continue since a particular listener
                    // can only be added once
                    return;
                }
            }
        }
    };

/**

@property evento.EventTarget.prototype.addParentEventTarget

@parameter parent {EventTarget} A parent to call when bubbling an event.

@description

When an event is dispatched on an event target, if that event target has parents
then the event is also dispatched on the parents as long as bubbling has not
been canceled on the event.

One parent can be added only once.

var o = new evento.EventTarget();
et.addParentEventTarget(o);

*/
    evento.EventTarget.prototype.addParentEventTarget = function(parent) {
        if (typeof parent.dispatchEvent !== 'function') {
            throw new TypeError('evento.EventTarget.prototype.addParentEventTarget: Parents must have dispatchEvent method.');
        }
        hasOwnProperty(this, '_evento_parents') || (this._evento_parents = []);
        var parents = this._evento_parents;
        for (var i = 0, ilen = parents.length; i < ilen; i++) {
            if (parents[i] === parent) {
                // can only add a particular parent once
                return;
            }
        }
        parents.push(parent);
    };

/**

@property evento.EventTarget.prototype.removeParentEventTarget

@parameter parent {EventTarget} The parent to remove.

@description

Removes parent added with addParentEventTarget. If the listener is
not found there are no errors.

var o = {handleEvent:function(){}};
et.removeParentEventTarget(o);

*/
    evento.EventTarget.prototype.removeParentEventTarget = function(parent) {
        if (hasOwnProperty(this, '_evento_parents')) {
            var parents = this._evento_parents;
            for (var i = 0, ilen = parents.length; i < ilen; i++) {
                if (parents[i] === parent) {
                    parents.splice(i, 1);
                    // no need to continue as parent can be added only once
                    return;
                }
            }
        }
    };

/**

@property evento.EventTarget.prototype.dispatchEvent

@parameter evt {object} The event object to dispatch to all listeners.

@description

The evt.type property is required. All listeners registered for this
event type are called with evt passed as an argument to the listeners.

If not set, the evt.target property will be set to be the event target.

The evt.currentTarget will be set to be the event target.

Call evt.stopPropagation() to stop bubbling to parents.

et.dispatchEvent({type:'change'});
et.dispatchEvent({type:'change', extraData:'abc'});

*/
    evento.EventTarget.prototype.dispatchEvent = function(evt) {
        // Want to ensure we don't alter the evt object passed in as it
        // may be a bubbling event. So clone it and then setting currentTarget
        // won't break some event that is already being dispatched.
        evt = create(evt);
        ('target' in evt) || (evt.target = this); // don't change target on bubbling event
        evt.currentTarget = this;
        evt._propagationStopped = ('bubbles' in evt) ? !evt.bubbles : false;
        evt.stopPropagation = function() {
            evt._propagationStopped = true;
        };
        if (hasOwnProperty(this, '_evento_listeners') &&
            hasOwnProperty(this._evento_listeners, evt.type)) {
            // Copy the list of listeners in case one of the
            // listeners modifies the list while we are
            // iterating over the list.
            //
            // Without making a copy, one listener removing
            // an already-called listener would result in skipping
            // a not-yet-called listener. One listener removing
            // a not-yet-called listener would result in skipping that
            // not-yet-called listner. The worst case scenario
            // is a listener adding itself again which would
            // create an infinite loop.
            //
            var listeners = this._evento_listeners[evt.type].slice(0);
            for (var i = 0, ilen = listeners.length; i < ilen; i++) {
                var listener = listeners[i];
                if (typeof listener === 'function') {
                    listener.call(this, evt);
                }
                else {
                    listener.handleEvent(evt);
                }
            }
        }
        if (hasOwnProperty(this, '_evento_parents') &&
            !evt._propagationStopped) {
            for (var i=0, ilen=this._evento_parents.length; i<ilen; i++) {
                this._evento_parents[i].dispatchEvent(evt);
            }
        }
    };

/**

@property evento.EventTarget.mixin

@parameter obj {object} The object to be made into an event target.

@description

Mixes in the event target methods into any object.

var o = {};
evento.EventTarget.mixin(o);
o.addEventListener('change', function(){alert('change');});
o.dispatchEvent({type:'change'});

*/
    evento.EventTarget.mixin = function(obj) {
        var pt = evento.EventTarget.prototype;
        for (var p in pt) {
            if (hasOwnProperty(pt, p) &&
                // Don't want to copy evento.EventTarget.prototype._evento_listeners object. Want the obj object
                // to have its own listeners and not share listeners with evento.EventTarget.prototype.
                (typeof pt[p] === 'function')) {
                obj[p] = pt[p];
            }
        }
    };

}());
<commit_msg>update evento.EventTarget<commit_after>/*
 * Copyright (c) 2012, Peter Michaux, http://peter.michaux.ca/
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

/**

@property evento.EventTarget

@description

A constructor function for creating event target objects.

var et = new evento.EventTarget();

The methods of an event target object are inspired by the DOM2 standard.

*/
evento.EventTarget = function() {};

(function() {

    function hasOwnProperty(o, p) {
        return Object.prototype.hasOwnProperty.call(o, p);
    }

    var create = (function() {
        function F() {}
        return function(o) {
            F.prototype = o;
            return new F();
        };
    }());

/**

@property evento.EventTarget.prototype.addEventListener

@parameter type {string} The name of the event.

@parameter listener {object|function} The listener object or callback function.

@description

If the listener is an object then when a matching event type is dispatched on
the event target, the listener object's handleEvent method will be called.

If the listener is a function then when a matching event type is dispatched on
the event target, the listener function is called with event target object set as
the "this" object.

One listener (or type/listener pair to be more precise) can be added only once.

et.addEventListener('change', {handleEvent:function(){}});
et.addEventListener('change', function(){});

*/
    evento.EventTarget.prototype.addEventListener = function(type, listener) {
        hasOwnProperty(this, '_evento_listeners') || (this._evento_listeners = {});
        hasOwnProperty(this._evento_listeners, type) || (this._evento_listeners[type] = []);
        var listeners = this._evento_listeners[type];
        for (var i = 0, ilen = listeners.length; i < ilen; i++) {
            if (listeners[i] === listener) {
                // can only add a listener once
                return;
            }
        }
        listeners.push(listener);
    };

/**

@property evento.EventTarget.prototype.removeEventListener

@parameter type {string} The name of the event.

@parameter listener {object|function} The listener object or callback function.

@description

Removes added listener matching the type/listener combination exactly.
If this combination is not found there are no errors.

var o = {handleEvent:function(){}};
et.removeEventListener('change', o);
et.removeEventListener('change', fn);

*/
    evento.EventTarget.prototype.removeEventListener = function(type, listener) {
        if (hasOwnProperty(this, '_evento_listeners') &&
            hasOwnProperty(this._evento_listeners, type)) {
            var listeners = this._evento_listeners[type];
            for (var i = 0, ilen = listeners.length; i < ilen; i++) {
                if (listeners[i] === listener) {
                    listeners.splice(i, 1);
                    // no need to continue since a particular listener
                    // can only be added once
                    return;
                }
            }
        }
    };

/**

@property evento.EventTarget.prototype.addParentEventTarget

@parameter parent {EventTarget} A parent to call when bubbling an event.

@description

When an event is dispatched on an event target, if that event target has parents
then the event is also dispatched on the parents as long as bubbling has not
been canceled on the event.

One parent can be added only once.

var o = new evento.EventTarget();
et.addParentEventTarget(o);

*/
    evento.EventTarget.prototype.addParentEventTarget = function(parent) {
        if (typeof parent.dispatchEvent !== 'function') {
            throw new TypeError('evento.EventTarget.prototype.addParentEventTarget: Parents must have dispatchEvent method.');
        }
        hasOwnProperty(this, '_evento_parents') || (this._evento_parents = []);
        var parents = this._evento_parents;
        for (var i = 0, ilen = parents.length; i < ilen; i++) {
            if (parents[i] === parent) {
                // can only add a particular parent once
                return;
            }
        }
        parents.push(parent);
    };

/**

@property evento.EventTarget.prototype.removeParentEventTarget

@parameter parent {EventTarget} The parent to remove.

@description

Removes parent added with addParentEventTarget. If the listener is
not found there are no errors.

var o = {handleEvent:function(){}};
et.removeParentEventTarget(o);

*/
    evento.EventTarget.prototype.removeParentEventTarget = function(parent) {
        if (hasOwnProperty(this, '_evento_parents')) {
            var parents = this._evento_parents;
            for (var i = 0, ilen = parents.length; i < ilen; i++) {
                if (parents[i] === parent) {
                    parents.splice(i, 1);
                    // no need to continue as parent can be added only once
                    return;
                }
            }
        }
    };

/**

@property evento.EventTarget.prototype.dispatchEvent

@parameter evt {object} The event object to dispatch to all listeners.

@description

The evt.type property is required. All listeners registered for this
event type are called with evt passed as an argument to the listeners.

If not set, the evt.target property will be set to be the event target.

The evt.currentTarget will be set to be the event target.

Call evt.stopPropagation() to stop bubbling to parents.

et.dispatchEvent({type:'change'});
et.dispatchEvent({type:'change', extraData:'abc'});

*/
    evento.EventTarget.prototype.dispatchEvent = function(evt) {
        // Want to ensure we don't alter the evt object passed in as it
        // may be a bubbling event. So clone it and then setting currentTarget
        // won't break some event that is already being dispatched.
        evt = create(evt);
        ('target' in evt) || (evt.target = this); // don't change target on bubbling event
        evt.currentTarget = this;
        evt._propagationStopped = ('bubbles' in evt) ? !evt.bubbles : false;
        evt.stopPropagation = function() {
            evt._propagationStopped = true;
        };
        if (hasOwnProperty(this, '_evento_listeners') &&
            hasOwnProperty(this._evento_listeners, evt.type)) {
            // Copy the list of listeners in case one of the
            // listeners modifies the list while we are
            // iterating over the list.
            //
            // Without making a copy, one listener removing
            // an already-called listener would result in skipping
            // a not-yet-called listener. One listener removing
            // a not-yet-called listener would result in skipping that
            // not-yet-called listner. The worst case scenario
            // is a listener adding itself again which would
            // create an infinite loop.
            //
            var listeners = this._evento_listeners[evt.type].slice(0);
            for (var i = 0, ilen = listeners.length; i < ilen; i++) {
                var listener = listeners[i];
                if (typeof listener === 'function') {
                    listener.call(this, evt);
                }
                else {
                    listener.handleEvent(evt);
                }
            }
        }
        if (hasOwnProperty(this, '_evento_parents') &&
            !evt._propagationStopped) {
            for (var i=0, ilen=this._evento_parents.length; i<ilen; i++) {
                this._evento_parents[i].dispatchEvent(evt);
            }
        }
    };

/**

@property evento.EventTarget.mixin

@parameter obj {object} The object to be made into an event target.

@description

Mixes in the event target methods into any object.

// Example 1

app.Person = function(name) {
    evento.EventTarget.call(this);
    this.setName(name);
};
evento.EventTarget.mixin(app.Person.prototype);
app.Person.prototype.setName = function(newName) {
    var oldName = this.name;
    this.name = newName;
    this.dispatchEvent({
        type: "change",
        oldName: oldName,
        newName: newName
    });
};

var person = new app.Person('David');
person.addEventListener('change', function(evt) {
    alert('"' + evt.oldName + '" is now called "' + evt.newName + '".');
});
person.setName('Dave');

// Example 2

var o = {};
evento.EventTarget.mixin(o);
o.addEventListener('change', function(){alert('change');});
o.dispatchEvent({type:'change'});

*/
    evento.EventTarget.mixin = function(obj) {
        var pt = evento.EventTarget.prototype;
        for (var p in pt) {
            if (hasOwnProperty(pt, p) &&
                // Don't want to copy evento.EventTarget.prototype._evento_listeners object
                // or the evento.EventTarget.prototype._evento_parents object. Want the obj object
                // to have its own listeners and parents and not share listeners and parents
                // with evento.EventTarget.prototype.
                (typeof pt[p] === 'function')) {
                obj[p] = pt[p];
            }
        }
        evento.EventTarget.call(obj);
    };

}());
<|endoftext|>