<commit_before>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */
'use strict';

const Systrace = require('../Performance/Systrace');

const infoLog = require('./infoLog');
const performanceNow =
  global.nativeQPLTimestamp ||
  global.nativePerformanceNow ||
  require('fbjs/lib/performanceNow');

type Timespan = {
  description?: string,
  totalTime?: number,
  startTime?: number,
  endTime?: number,
};

export type IPerformanceLogger = {
  addTimespan(string, number, string | void): void,
  startTimespan(string, string | void): void,
  stopTimespan(string): void,
  clear(): void,
  clearCompleted(): void,
  clearExceptTimespans(Array<string>): void,
  currentTimestamp(): number,
  getTimespans(): {[key: string]: Timespan},
  hasTimespan(string): boolean,
  logTimespans(): void,
  addTimespans(Array<number>, Array<string>): void,
  setExtra(string, any): void,
  getExtras(): {[key: string]: any},
  removeExtra(string): ?any,
  logExtras(): void,
  markPoint(string, number | void): void,
  getPoints(): {[key: string]: number},
  logPoints(): void,
  logEverything(): void,
};

const _cookies: {[key: string]: number} = {};

const PRINT_TO_CONSOLE: false = false; // Type as false to prevent accidentally committing `true`;

/**
 * This function creates peformance loggers that can be used to collect and log
 * various performance data such as timespans, points and extras.
 * The loggers need to have minimal overhead since they're used in production.
 */
function createPerformanceLogger(): IPerformanceLogger {
  const result: IPerformanceLogger & {
    _timespans: {[key: string]: Timespan},
    _extras: {[key: string]: any},
    _points: {[key: string]: number},
  } = {
    _timespans: {},
    _extras: {},
    _points: {},

    addTimespan(key: string, lengthInMs: number, description?: string) {
      if (this._timespans[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to add a timespan that already exists ',
            key,
          );
        }
        return;
      }

      this._timespans[key] = {
        description: description,
        totalTime: lengthInMs,
      };
    },

    startTimespan(key: string, description?: string) {
      if (this._timespans[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to start a timespan that already exists ',
            key,
          );
        }
        return;
      }

      this._timespans[key] = {
        description: description,
        startTime: performanceNow(),
      };
      _cookies[key] = Systrace.beginAsyncEvent(key);
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'start: ' + key);
      }
    },

    stopTimespan(key: string) {
      const timespan = this._timespans[key];
      if (!timespan || !timespan.startTime) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to end a timespan that has not started ',
            key,
          );
        }
        return;
      }
      if (timespan.endTime) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to end a timespan that has already ended ',
            key,
          );
        }
        return;
      }

      timespan.endTime = performanceNow();
      timespan.totalTime = timespan.endTime - (timespan.startTime || 0);
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'end: ' + key);
      }

      Systrace.endAsyncEvent(key, _cookies[key]);
      delete _cookies[key];
    },

    clear() {
      this._timespans = {};
      this._extras = {};
      this._points = {};
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'clear');
      }
    },

    clearCompleted() {
      for (const key in this._timespans) {
        if (this._timespans[key].totalTime) {
          delete this._timespans[key];
        }
      }
      this._extras = {};
      this._points = {};
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'clearCompleted');
      }
    },

    clearExceptTimespans(keys: Array<string>) {
      this._timespans = Object.keys(this._timespans).reduce(function(
        previous,
        key,
      ) {
        if (keys.indexOf(key) !== -1) {
          previous[key] = this._timespans[key];
        }
        return previous;
      },
      {});
      this._extras = {};
      this._points = {};
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'clearExceptTimespans', keys);
      }
    },

    currentTimestamp() {
      return performanceNow();
    },

    getTimespans() {
      return this._timespans;
    },

    hasTimespan(key: string) {
      return !!this._timespans[key];
    },

    logTimespans() {
      for (const key in this._timespans) {
        if (this._timespans[key].totalTime) {
          infoLog(key + ': ' + this._timespans[key].totalTime + 'ms');
        }
      }
    },

    addTimespans(newTimespans: Array<number>, labels: Array<string>) {
      for (let ii = 0, l = newTimespans.length; ii < l; ii += 2) {
        const label = labels[ii / 2];
        this.addTimespan(label, newTimespans[ii + 1] - newTimespans[ii], label);
      }
    },

    setExtra(key: string, value: any) {
      if (this._extras[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to set an extra that already exists ',
            {key, currentValue: this._extras[key], attemptedValue: value},
          );
        }
        return;
      }
      this._extras[key] = value;
    },

    getExtras() {
      return this._extras;
    },

    removeExtra(key: string): ?any {
      const value = this._extras[key];
      delete this._extras[key];
      return value;
    },

    logExtras() {
      infoLog(this._extras);
    },

    markPoint(key: string, timestamp?: number) {
      if (this._points[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to mark a point that has been already logged ',
            key,
          );
        }
        return;
      }
      this._points[key] = timestamp ?? performanceNow();
    },

    getPoints() {
      return this._points;
    },

    logPoints() {
      for (const key in this._points) {
        infoLog(key + ': ' + this._points[key] + 'ms');
      }
    },

    logEverything() {
      this.logTimespans();
      this.logExtras();
      this.logPoints();
    },
  };
  return result;
}

module.exports = createPerformanceLogger;
<commit_msg>Disable logging from performance logger unless it is enabled<commit_after>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */
'use strict';

const Systrace = require('../Performance/Systrace');

const infoLog = require('./infoLog');
const performanceNow =
  global.nativeQPLTimestamp ||
  global.nativePerformanceNow ||
  require('fbjs/lib/performanceNow');

type Timespan = {
  description?: string,
  totalTime?: number,
  startTime?: number,
  endTime?: number,
};

export type IPerformanceLogger = {
  addTimespan(string, number, string | void): void,
  startTimespan(string, string | void): void,
  stopTimespan(string): void,
  clear(): void,
  clearCompleted(): void,
  clearExceptTimespans(Array<string>): void,
  currentTimestamp(): number,
  getTimespans(): {[key: string]: Timespan},
  hasTimespan(string): boolean,
  logTimespans(): void,
  addTimespans(Array<number>, Array<string>): void,
  setExtra(string, any): void,
  getExtras(): {[key: string]: any},
  removeExtra(string): ?any,
  logExtras(): void,
  markPoint(string, number | void): void,
  getPoints(): {[key: string]: number},
  logPoints(): void,
  logEverything(): void,
};

const _cookies: {[key: string]: number} = {};

const PRINT_TO_CONSOLE: false = false; // Type as false to prevent accidentally committing `true`;

/**
 * This function creates peformance loggers that can be used to collect and log
 * various performance data such as timespans, points and extras.
 * The loggers need to have minimal overhead since they're used in production.
 */
function createPerformanceLogger(): IPerformanceLogger {
  const result: IPerformanceLogger & {
    _timespans: {[key: string]: Timespan},
    _extras: {[key: string]: any},
    _points: {[key: string]: number},
  } = {
    _timespans: {},
    _extras: {},
    _points: {},

    addTimespan(key: string, lengthInMs: number, description?: string) {
      if (this._timespans[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to add a timespan that already exists ',
            key,
          );
        }
        return;
      }

      this._timespans[key] = {
        description: description,
        totalTime: lengthInMs,
      };
    },

    startTimespan(key: string, description?: string) {
      if (this._timespans[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to start a timespan that already exists ',
            key,
          );
        }
        return;
      }

      this._timespans[key] = {
        description: description,
        startTime: performanceNow(),
      };
      _cookies[key] = Systrace.beginAsyncEvent(key);
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'start: ' + key);
      }
    },

    stopTimespan(key: string) {
      const timespan = this._timespans[key];
      if (!timespan || !timespan.startTime) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to end a timespan that has not started ',
            key,
          );
        }
        return;
      }
      if (timespan.endTime) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to end a timespan that has already ended ',
            key,
          );
        }
        return;
      }

      timespan.endTime = performanceNow();
      timespan.totalTime = timespan.endTime - (timespan.startTime || 0);
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'end: ' + key);
      }

      Systrace.endAsyncEvent(key, _cookies[key]);
      delete _cookies[key];
    },

    clear() {
      this._timespans = {};
      this._extras = {};
      this._points = {};
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'clear');
      }
    },

    clearCompleted() {
      for (const key in this._timespans) {
        if (this._timespans[key].totalTime) {
          delete this._timespans[key];
        }
      }
      this._extras = {};
      this._points = {};
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'clearCompleted');
      }
    },

    clearExceptTimespans(keys: Array<string>) {
      this._timespans = Object.keys(this._timespans).reduce(function(
        previous,
        key,
      ) {
        if (keys.indexOf(key) !== -1) {
          previous[key] = this._timespans[key];
        }
        return previous;
      },
      {});
      this._extras = {};
      this._points = {};
      if (PRINT_TO_CONSOLE) {
        infoLog('PerformanceLogger.js', 'clearExceptTimespans', keys);
      }
    },

    currentTimestamp() {
      return performanceNow();
    },

    getTimespans() {
      return this._timespans;
    },

    hasTimespan(key: string) {
      return !!this._timespans[key];
    },

    logTimespans() {
      if (PRINT_TO_CONSOLE) {
        for (const key in this._timespans) {
          if (this._timespans[key].totalTime) {
            infoLog(key + ': ' + this._timespans[key].totalTime + 'ms');
          }
        }
      }
    },

    addTimespans(newTimespans: Array<number>, labels: Array<string>) {
      for (let ii = 0, l = newTimespans.length; ii < l; ii += 2) {
        const label = labels[ii / 2];
        this.addTimespan(label, newTimespans[ii + 1] - newTimespans[ii], label);
      }
    },

    setExtra(key: string, value: any) {
      if (this._extras[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to set an extra that already exists ',
            {key, currentValue: this._extras[key], attemptedValue: value},
          );
        }
        return;
      }
      this._extras[key] = value;
    },

    getExtras() {
      return this._extras;
    },

    removeExtra(key: string): ?any {
      const value = this._extras[key];
      delete this._extras[key];
      return value;
    },

    logExtras() {
      if (PRINT_TO_CONSOLE) {
        infoLog(this._extras);
      }
    },

    markPoint(key: string, timestamp?: number) {
      if (this._points[key]) {
        if (PRINT_TO_CONSOLE && __DEV__) {
          infoLog(
            'PerformanceLogger: Attempting to mark a point that has been already logged ',
            key,
          );
        }
        return;
      }
      this._points[key] = timestamp ?? performanceNow();
    },

    getPoints() {
      return this._points;
    },

    logPoints() {
      if (PRINT_TO_CONSOLE) {
        for (const key in this._points) {
          infoLog(key + ': ' + this._points[key] + 'ms');
        }
      }
    },

    logEverything() {
      this.logTimespans();
      this.logExtras();
      this.logPoints();
    },
  };
  return result;
}

module.exports = createPerformanceLogger;
<|endoftext|>


<commit_before>'use strict';

const makeLoaderFinder = loaderName => rule => {
  // i.e.: /eslint-loader/
  const loaderRegex = new RegExp(`[/\\\\]${loaderName}[/\\\\]`);

  // Checks if there's a loader string in rule.loader matching loaderRegex.
  const inLoaderString =
    typeof rule.loader === 'string' && rule.loader.match(loaderRegex);

  // Checks if there is an object inside rule.use with loader matching loaderRegex, OR
  // Checks another condition, if rule is not an object, but pure string (ex: "style-loader", etc)
  const inUseArray =
    Array.isArray(rule.use) &&
    rule.use.find(
      loader =>
        (typeof loader.loader === 'string' &&
          loader.loader.match(loaderRegex)) ||
        (typeof loader === 'string' && loader.match(loaderRegex))
    );

  return inUseArray || inLoaderString;
};

module.exports = makeLoaderFinder;
<commit_msg>fix: allow getting string only loaders<commit_after>'use strict';

const makeLoaderFinder = loaderName => rule => {
  // i.e.: /eslint-loader/
  const loaderRegex = new RegExp(`[/\\\\]${loaderName}[/\\\\]`);

  // Checks if there's a loader string in rule.loader matching loaderRegex.
  const inLoaderString =
    typeof rule.loader === 'string' && (rule.loader.match(loaderRegex) || rule.loader === loaderName);

  // Checks if there is an object inside rule.use with loader matching loaderRegex, OR
  // Checks another condition, if rule is not an object, but pure string (ex: "style-loader", etc)
  const inUseArray =
    Array.isArray(rule.use) &&
    rule.use.find(
      loader =>
        (typeof loader.loader === 'string' &&
          (loader.loader.match(loaderRegex)) || rule.loader === loaderName) ||
        (typeof loader === 'string' && (loader.match(loaderRegex) || loader === loaderName))
    );

  return inUseArray || inLoaderString;
};

module.exports = makeLoaderFinder;
<|endoftext|>


<commit_before>// Rebound Router
// ----------------

import $ from "rebound-component/utils";
import LazyComponent from "rebound-router/lazy-component";

var DEFAULT_404_PAGE =
`

    Oops! We couldn't find this page.

    Take me back

`;

var ERROR_ROUTE_NAME = 'error';
var SUCCESS = 'success';
var ERROR = 'error';
var LOADING = 'loading';

// Overload Backbone's loadUrl so it returns the value of the routed callback
// instead of undefined and prefixes all fragment tests with the current app name
Backbone.history.loadUrl = function(fragment) {
  fragment = this.fragment = this.getFragment(fragment);
  var resp = false;

  _.any(this.handlers, function(handler) {
    if (handler.route.test(fragment)) {
      resp = handler.callback(fragment);
      return true;
    }
  });

  return resp;
};

// ReboundRouter Constructor
var ReboundRouter = Backbone.Router.extend({

  status: SUCCESS,    // loading, success or error
  _currentRoute:  '', // The route path that triggered the current page
  _previousRoute: '',

  // By default there is one route. The wildcard route fetches the required
  // page assets based on user-defined naming convention.
  routes: {
    '*route': 'wildcardRoute'
  },

  // Called when no matching routes are found. Extracts root route and fetches it's resources
  wildcardRoute: function(route) {

    // Save the previous route value
    this._previousRoute = this._currentRoute;

    // Fetch Resources
    document.body.classList.add("loading");
    return this._fetchResource(route, this.config.container).then(function(res){
      document.body.classList.remove('loading');
      return res;
    });
  },

  // Modify navigate to default to `trigger=true` and to return the value of
  // `Backbone.history.navigate` inside of a promise.
  navigate: function(fragment, options={}) {
    (options.trigger === undefined) && (options.trigger = true);
    var resp = Backbone.history.navigate(fragment, options);
    // Always return a promise
    return new Promise(function(resolve, reject) {
      if(resp && resp.constructor === Promise) resp.then(resolve, resolve);
      resolve(resp);
    });
  },

  // Modify `router.execute` to return the value of our route callback
  execute: function(callback, args, name) {
    if (callback) return callback.apply(this, args);
  },

  // Override route so if callback returns false, the route event is not triggered
  route: function(route, name, callback) {
    if (!_.isRegExp(route)) route = this._routeToRegExp(route);
    if (_.isFunction(name)) {
      callback = name;
      name = '';
    }

    if (!callback) callback = this[name];
    Backbone.history.route(route, (fragment) => {
      var args = this._extractParameters(route, fragment);
      var resp = this.execute(callback, args, name);
      if ( resp !== false) {
        this.trigger.apply(this, ['route:' + name].concat(args));
        this.trigger('route', name, args);
        Backbone.history.trigger('route', this, name, args);
      }
      return resp;
    });
    return this;
  },

  // On startup, save our config object and start the router
  initialize: function(options={}, callback=function(){}) {

    // Let all of our components always have referance to our router
    Rebound.Component.prototype.router = this;

    // Save our config referance
    this.config = options;
    this.config.handlers = [];

    // Get a unique instance id for this router
    this.uid = _.uniqueId('router');

    // Allow user to override error route
    ERROR_ROUTE_NAME = this.config.errorRoute || ERROR_ROUTE_NAME;

    // Use the user provided container, or default to the closest `<main>` tag
    var container = this.config.container = $((this.config.container || 'main'))[0];

    // Convert our routeMappings to regexps and push to our handlers
    _.each(this.config.routeMapping, function(value, route){
      var regex = this._routeToRegExp(route);
      this.config.handlers.unshift({ route: route, regex: regex, app: value });
    }, this);

    this._watchLinks(container);
    Rebound.services.page = new LazyComponent();

    // Install our global components
    _.each(this.config.services, function(selector, route){
      var container = $(selector)[0] || document.createElement('span');
      this._watchLinks(container);
      Rebound.services[route] = new LazyComponent();
      this._fetchResource(route, container).catch(function(){});
    }, this);

    // Start the history and call the provided callback
    Backbone.history.start({
      pushState: (this.config.pushState === undefined) ? true : this.config.pushState,
      root: this.config.root
    }).then(callback);

    return this;
  },

  stop: function(){
    $(this.config.container).off('click');
    Backbone.history.stop();
    this._uninstallResource();
    Backbone.history.handlers = [];
  },

  // Given a dom element, watch for all click events on anchor tags.
  // If the clicked anchor has a relative url, attempt to route to that path.
  // Give all links on the page that match this path the class `active`.
  _watchLinks: function(container){
    // Navigate to route for any link with a relative href
    var remoteUrl = /^([a-z]+:)|^(\/\/)|^([^\/]+\.)/;
    $(container).on('click', 'a', (e) => {
      var path = e.target.getAttribute('href');
      // If path is not an remote url, ends in .[a-z], or blank, try and navigate to that route.
      if( path && path !== '#' && !remoteUrl.test(path) ) e.preventDefault();
      // If this is not our current route, navigate to the new route
      if(path !== '/'+Backbone.history.fragment){
        $(container).unMarkLinks();
        this.navigate(path, {trigger: true})
          .then(function(){ $(container).markLinks(); });
      }
    });
  },

  // De-initializes the previous app before rendering a new app
  // This way we can ensure that every new page starts with a clean slate
  // This is crucial for scalability of a single page app.
  _uninstallResource: function(){

    var routes = this.current ? (this.current.data.routes || {}) : {};
        routes[this._previousRoute] = '';

    // Unset Previous Application's Routes. For each route in the page app, remove
    // the handler from our route object and delete our referance to the route's callback
    _.each(routes, (value, key) => {
      if(key[0] === '/') key = new RegExp(key.split('/')[1], key.split('/')[2]);
      var regExp = (key instanceof RegExp) ? key.toString() : this._routeToRegExp(key).toString();
      Backbone.history.handlers = _.filter(Backbone.history.handlers, function(obj){
        return obj.route.toString() !== regExp;
      });
    });

    if(!this.current) return;

    var oldPageName = this.current.__name

    // Un-hook Event Bindings, Delete Objects
    this.current.data.deinitialize();

    // Now we no longer have a page installed.
    this.current = undefined;

    // Disable old css if it exists
    setTimeout(() => {
      if(this.status === ERROR) return;
      document.getElementById(oldPageName + '-css').setAttribute('disabled', true);
    }, 500);

  },

  // Give our new page component, load routes and render a new instance of the
  // page component in the top level outlet.
  _installResource: function(PageApp, appName, container) {
    var oldPageName, pageInstance, routes = [];
    var isService = (container !== this.config.container);
    container.classList.remove('error', 'loading');

    if(!isService && this.current) this._uninstallResource();

    // Load New PageApp, give it it's name so we know what css to remove when it deinitializes
    pageInstance = new PageApp();
    pageInstance.__name = this.uid + '-' + appName;

    // Add to our page
    container.innerHTML = '';
    container.appendChild(pageInstance);

    // Make sure we're back at the top of the page
    document.body.scrollTop = 0;

    // Augment ApplicationRouter with new routes from PageApp added in reverse order to preserve order higherarchy
    if(!isService) this.route(this._currentRoute, 'default', function(){ return 'DEFAULT'; });
    _.each(pageInstance.data.routes, (value, key) => {
      // If key is a stringified regexp literal, convert to a regexp object
      if(key[0] === '/') key = new RegExp(key.split('/')[1], key.split('/')[2]);
      routes.unshift({key: key, value: value});
      // Add the new callback referance on to our router and add the route handler
    }, this);
    _.each(routes, (route) => {
      this.route(route.key, route.value, function () { return pageInstance.data[route.value].apply(pageInstance.data, arguments); });
    });

    var name = (isService) ? appName : 'page';
    if(!isService) this.current = pageInstance;

    // If the target is a dummy service, hydrate it with the proper service object
    // Otherwise, install the page instance here
    if(window.Rebound.services[name].isService)
      window.Rebound.services[name].hydrate(pageInstance.data);
    window.Rebound.services[name] = pageInstance.data;

    // Always return a promise
    return new Promise(function(resolve, reject){

      // Re-trigger route so the newly added route may execute if there's a route match.
      // If no routes are matched, app will hit wildCard route which will then trigger 404
      if(!isService){
        let res = Backbone.history.loadUrl(Backbone.history.fragment);
        if(res && typeof res.then === 'function') return res.then(resolve);
        return resolve(res);
      }
      // Return our newly installed app
      return resolve(pageInstance);
    });
  },

  _fetchJavascript: function(routeName, appName){
    var jsID = this.uid + '-' + appName + '-js',
        jsUrl = this.config.jsPath.replace(/:route/g, routeName).replace(/:app/g, appName),
        jsElement = document.getElementById(appName + '-js');

    // AMD will manage dependancies for us. Load the JavaScript.
    return new Promise(function(resolve, reject){
      window.require([jsUrl], (PageClass) => {
        jsElement = $('script[src="'+jsUrl+'"]')[0];
        jsElement.setAttribute('id', jsID);
        resolve(PageClass);
      }, function(err){
        console.error(err);
        reject(err);
      });
    });
  },

  _fetchCSS: function(routeName, appName){

    var cssID = this.uid + '-' + appName + '-css',
        cssUrl = this.config.cssPath.replace(/:route/g, routeName).replace(/:app/g, appName),
        cssElement = document.getElementById(cssID);

    // If this css element is not on the page already, it hasn't been loaded before -
    // create the element and load the css resource.
    // Else if the css resource has been loaded before, enable it
    return new Promise(function(resolve, reject){
      var count = 0, ti;
      if(cssElement === null){
        cssElement = document.createElement('link');
        cssElement.setAttribute('type', 'text/css');
        cssElement.setAttribute('rel', 'stylesheet');
        cssElement.setAttribute('href', cssUrl);
        cssElement.setAttribute('id', cssID);
        var successCallback = function(){
          clearInterval(ti);
          resolve(cssElement);
        }
        var errorCallback = function(err){
          clearInterval(ti);
          cssElement.dataset.error = '';
          reject(err);
        }

        // Older browsers and phantomJS < 2.0 don't support the onLoad event for
        // `<link>` tags. Pool stylesheets array as a fallback. Timeout at 5s.
        ti = setInterval(function() {
          for(var i = 0; i < document.styleSheets.length; i++){
            count = count + 50;
            if(document.styleSheets[i].href.indexOf(cssUrl) > -1) successCallback();
            else if(count >= 5000) errorCallback('CSS Timeout')
          }
        }, 50);

        $(cssElement).on('load', successCallback);
        $(cssElement).on('error', errorCallback);
        $(cssElement).on('readystatechange', function(){ clearInterval(ti); })

        document.head.appendChild(cssElement);

      } else {
        if(cssElement.hasAttribute('data-error')) return reject();
        resolve(cssElement);
      }
    });
  },

  // Fetches HTML and CSS
  _fetchResource: function(route, container) {

    var appName, routeName,
        isService = (container !== this.config.container),
        isError = (route === ERROR_ROUTE_NAME);

    // Normalize Route
    route || (route = '');

    // Get the app name from this route
    appName = routeName = (route.split('/')[0] || 'index');

    // If this isn't the error route, Find Any Custom Route Mappings
    if(!isService && !isError){
      this._currentRoute = route.split('/')[0];
      _.any(this.config.handlers, (handler) => {
        if (handler.regex.test(route)){
          appName = handler.app;
          this._currentRoute = handler.route;
          return true;
        }
      });
    }

    // Wrap these async resource fetches in a promise and return it.
    // This promise resolves when both css and js resources are loaded
    // It rejects if either of the css or js resources fails to load.
    return new Promise((resolve, reject) => {

      var throwError = (err) => {
        // If we are already in an error state, this means we were unable to load
        // a custom error page. Uninstall anything we have and insert our default 404 page.
        if(this.status === ERROR){
          if(isService) return resolve(err);
          this._uninstallResource();
          container.innerHTML = DEFAULT_404_PAGE;
          return resolve(err);
        }

        // Set our status to error and attempt to load a custom error page.
        console.error('Could not ' + ((isService) ? 'load the ' + appName + ' service:' : 'find the ' + appName + ' app.', 'at', ('/' + route)));
        this.status = ERROR;
        this._currentRoute = route;
        resolve(this._fetchResource(ERROR_ROUTE_NAME, container));
      };

      // If the values we got from installing our resources are unexpected, 404
      // Otherwise, set status, activate the css, and install the page component
      var install = (response) => {
        var cssElement = response[0], PageClass = response[1];
        if(!(cssElement instanceof Element) || typeof PageClass !== 'function') return throwError();
        (!isService && !isError) && (this.status = SUCCESS);
        cssElement && cssElement.removeAttribute('disabled');

        this._installResource(PageClass, appName, container).then(resolve, resolve);
      };

      // If loading a page, set status to loading
      (!isService && !isError) && (this.status = LOADING);

      // If Page Is Already Loaded Then The Route Does Not Exist. 404 and Exit.
      if (this.current && this.current.__name === (this.uid + '-' + appName)) return throwError();
      // Fetch our css and js in paralell, install or throw when both complete
      Promise.all([this._fetchCSS(routeName, appName), this._fetchJavascript(routeName, appName)])
      .then(install, throwError);

    });
  }
});

export default ReboundRouter;
<commit_msg>Added some comments to router, mostly just to kick off another build.<commit_after>// Rebound Router
// ----------------

import $ from "rebound-component/utils";
import LazyComponent from "rebound-router/lazy-component";

var DEFAULT_404_PAGE =
`

    Oops! We couldn't find this page.

    Take me back

`;

var ERROR_ROUTE_NAME = 'error';
var SUCCESS = 'success';
var ERROR = 'error';
var LOADING = 'loading';

// Overload Backbone's loadUrl so it returns the value of the routed callback
// instead of undefined and prefixes all fragment tests with the current app name
Backbone.history.loadUrl = function(fragment) {
  fragment = this.fragment = this.getFragment(fragment);
  var resp = false;

  _.any(this.handlers, function(handler) {
    if (handler.route.test(fragment)) {
      resp = handler.callback(fragment);
      return true;
    }
  });

  return resp;
};

// ReboundRouter Constructor
var ReboundRouter = Backbone.Router.extend({

  status: SUCCESS,    // loading, success or error
  _currentRoute:  '', // The route path that triggered the current page
  _previousRoute: '',

  // By default there is one route. The wildcard route fetches the required
  // page assets based on user-defined naming convention.
  routes: {
    '*route': 'wildcardRoute'
  },

  // Called when no matching routes are found. Extracts root route and fetches it's resources
  wildcardRoute: function(route) {

    // Save the previous route value
    this._previousRoute = this._currentRoute;

    // Fetch Resources
    document.body.classList.add("loading");
    return this._fetchResource(route, this.config.container).then(function(res){
      document.body.classList.remove('loading');
      return res;
    });
  },

  // Modify navigate to default to `trigger=true` and to return the value of
  // `Backbone.history.navigate` inside of a promise.
  navigate: function(fragment, options={}) {
    (options.trigger === undefined) && (options.trigger = true);
    var resp = Backbone.history.navigate(fragment, options);
    // Always return a promise
    return new Promise(function(resolve, reject) {
      if(resp && resp.constructor === Promise) resp.then(resolve, resolve);
      resolve(resp);
    });
  },

  // Modify `router.execute` to return the value of our route callback
  execute: function(callback, args, name) {
    if (callback) return callback.apply(this, args);
  },

  // Override route so if callback returns false, the route event is not triggered
  route: function(route, name, callback) {
    if (!_.isRegExp(route)) route = this._routeToRegExp(route);
    if (_.isFunction(name)) {
      callback = name;
      name = '';
    }

    if (!callback) callback = this[name];
    Backbone.history.route(route, (fragment) => {
      var args = this._extractParameters(route, fragment);
      var resp = this.execute(callback, args, name);
      if ( resp !== false) {
        this.trigger.apply(this, ['route:' + name].concat(args));
        this.trigger('route', name, args);
        Backbone.history.trigger('route', this, name, args);
      }
      return resp;
    });
    return this;
  },

  // On startup, save our config object and start the router
  initialize: function(options={}, callback=function(){}) {

    // Let all of our components always have referance to our router
    Rebound.Component.prototype.router = this;

    // Save our config referance
    this.config = options;
    this.config.handlers = [];

    // Get a unique instance id for this router
    this.uid = _.uniqueId('router');

    // Allow user to override error route
    ERROR_ROUTE_NAME = this.config.errorRoute || ERROR_ROUTE_NAME;

    // Use the user provided container, or default to the closest `<main>` tag
    var container = this.config.container = $((this.config.container || 'main'))[0];

    // Convert our routeMappings to regexps and push to our handlers
    _.each(this.config.routeMapping, function(value, route){
      var regex = this._routeToRegExp(route);
      this.config.handlers.unshift({ route: route, regex: regex, app: value });
    }, this);

    this._watchLinks(container);
    Rebound.services.page = new LazyComponent();

    // Install our global components
    _.each(this.config.services, function(selector, route){
      var container = $(selector)[0] || document.createElement('span');
      this._watchLinks(container);
      Rebound.services[route] = new LazyComponent();
      this._fetchResource(route, container).catch(function(){});
    }, this);

    // Start the history and call the provided callback
    Backbone.history.start({
      pushState: (this.config.pushState === undefined) ? true : this.config.pushState,
      root: this.config.root
    }).then(callback);

    return this;
  },

  stop: function(){
    $(this.config.container).off('click');
    Backbone.history.stop();
    this._uninstallResource();
    Backbone.history.handlers = [];
  },

  // Given a dom element, watch for all click events on anchor tags.
  // If the clicked anchor has a relative url, attempt to route to that path.
  // Give all links on the page that match this path the class `active`.
  _watchLinks: function(container){
    // Navigate to route for any link with a relative href
    var remoteUrl = /^([a-z]+:)|^(\/\/)|^([^\/]+\.)/;
    $(container).on('click', 'a', (e) => {
      var path = e.target.getAttribute('href');
      // If path is not an remote url, ends in .[a-z], or blank, try and navigate to that route.
      if( path && path !== '#' && !remoteUrl.test(path) ) e.preventDefault();
      // If this is not our current route, navigate to the new route
      if(path !== '/'+Backbone.history.fragment){
        $(container).unMarkLinks();
        this.navigate(path, {trigger: true})
          .then(function(){ $(container).markLinks(); });
      }
    });
  },

  // De-initializes the previous app before rendering a new app
  // This way we can ensure that every new page starts with a clean slate
  // This is crucial for scalability of a single page app.
  _uninstallResource: function(){

    var routes = this.current ? (this.current.data.routes || {}) : {};
        routes[this._previousRoute] = '';

    // Unset Previous Application's Routes. For each route in the page app, remove
    // the handler from our route object and delete our referance to the route's callback
    _.each(routes, (value, key) => {
      if(key[0] === '/') key = new RegExp(key.split('/')[1], key.split('/')[2]);
      var regExp = (key instanceof RegExp) ? key.toString() : this._routeToRegExp(key).toString();
      Backbone.history.handlers = _.filter(Backbone.history.handlers, function(obj){
        return obj.route.toString() !== regExp;
      });
    });

    if(!this.current) return;

    var oldPageName = this.current.__name

    // Un-hook Event Bindings, Delete Objects
    this.current.data.deinitialize();

    // Now we no longer have a page installed.
    this.current = undefined;

    // Disable old css if it exists
    setTimeout(() => {
      if(this.status === ERROR) return;
      document.getElementById(oldPageName + '-css').setAttribute('disabled', true);
    }, 500);

  },

  // Give our new page component, load routes and render a new instance of the
  // page component in the top level outlet.
  _installResource: function(PageApp, appName, container) {
    var oldPageName, pageInstance, routes = [];
    var isService = (container !== this.config.container);
    container.classList.remove('error', 'loading');

    if(!isService && this.current) this._uninstallResource();

    // Load New PageApp, give it it's name so we know what css to remove when it deinitializes
    pageInstance = new PageApp();
    pageInstance.__name = this.uid + '-' + appName;

    // Add to our page
    container.innerHTML = '';
    container.appendChild(pageInstance);

    // Make sure we're back at the top of the page
    document.body.scrollTop = 0;

    // Augment ApplicationRouter with new routes from PageApp added in reverse order to preserve order higherarchy
    if(!isService) this.route(this._currentRoute, 'default', function(){ return 'DEFAULT'; });
    _.each(pageInstance.data.routes, (value, key) => {
      // If key is a stringified regexp literal, convert to a regexp object
      if(key[0] === '/') key = new RegExp(key.split('/')[1], key.split('/')[2]);
      routes.unshift({key: key, value: value});
      // Add the new callback referance on to our router and add the route handler
    }, this);
    _.each(routes, (route) => {
      this.route(route.key, route.value, function () { return pageInstance.data[route.value].apply(pageInstance.data, arguments); });
    });

    var name = (isService) ? appName : 'page';
    if(!isService) this.current = pageInstance;

    // If the target is a dummy service, hydrate it with the proper service object
    // Otherwise, install the page instance here
    if(window.Rebound.services[name].isService)
      window.Rebound.services[name].hydrate(pageInstance.data);
    window.Rebound.services[name] = pageInstance.data;

    // Always return a promise
    return new Promise(function(resolve, reject){

      // Re-trigger route so the newly added route may execute if there's a route match.
      // If no routes are matched, app will hit wildCard route which will then trigger 404
      if(!isService){
        let res = Backbone.history.loadUrl(Backbone.history.fragment);
        if(res && typeof res.then === 'function') return res.then(resolve);
        return resolve(res);
      }
      // Return our newly installed app
      return resolve(pageInstance);
    });
  },

  _fetchJavascript: function(routeName, appName){
    var jsID = this.uid + '-' + appName + '-js',
        jsUrl = this.config.jsPath.replace(/:route/g, routeName).replace(/:app/g, appName),
        jsElement = document.getElementById(appName + '-js');

    // AMD will manage dependancies for us. Load the JavaScript.
    return new Promise(function(resolve, reject){
      window.require([jsUrl], (PageClass) => {
        jsElement = $('script[src="'+jsUrl+'"]')[0];
        jsElement.setAttribute('id', jsID);
        resolve(PageClass);
      }, function(err){
        console.error(err);
        reject(err);
      });
    });
  },

  _fetchCSS: function(routeName, appName){

    var cssID = this.uid + '-' + appName + '-css',
        cssUrl = this.config.cssPath.replace(/:route/g, routeName).replace(/:app/g, appName),
        cssElement = document.getElementById(cssID);

    // If this css element is not on the page already, it hasn't been loaded before -
    // create the element and load the css resource.
    // Else if the css resource has been loaded before, enable it
    return new Promise(function(resolve, reject){
      var count = 0, ti;
      if(cssElement === null){
        // Construct our `<link>` element.
        cssElement = document.createElement('link');
        cssElement.setAttribute('type', 'text/css');
        cssElement.setAttribute('rel', 'stylesheet');
        cssElement.setAttribute('href', cssUrl);
        cssElement.setAttribute('id', cssID);

        // On successful load, clearInterval and resolve.
        // On failed load, clearInterval and reject.
        var successCallback = function(){
          clearInterval(ti);
          resolve(cssElement);
        }
        var errorCallback = function(err){
          clearInterval(ti);
          cssElement.dataset.error = '';
          reject(err);
        }

        // Older browsers and phantomJS < 2.0 don't support the onLoad event for
        // `<link>` tags. Pool stylesheets array as a fallback. Timeout at 5s.
        ti = setInterval(function() {
          for(var i = 0; i < document.styleSheets.length; i++){
            count = count + 50;
            if(document.styleSheets[i].href.indexOf(cssUrl) > -1) successCallback();
            else if(count >= 5000) errorCallback('CSS Timeout')
          }
        }, 50);

        // Modern browsers support loading events on `<link>` elements, bind these
        // events. These will be callsed before our interval is called and they will
        // clearInterval so the resolve/reject handlers aren't called twice.
        $(cssElement).on('load', successCallback);
        $(cssElement).on('error', errorCallback);
        $(cssElement).on('readystatechange', function(){ clearInterval(ti); })

        // Add our `<link>` element to the page.
        document.head.appendChild(cssElement);

      } else {
        if(cssElement.hasAttribute('data-error')) return reject();
        resolve(cssElement);
      }
    });
  },

  // Fetches HTML and CSS
  _fetchResource: function(route, container) {

    var appName, routeName,
        isService = (container !== this.config.container),
        isError = (route === ERROR_ROUTE_NAME);

    // Normalize Route
    route || (route = '');

    // Get the app name from this route
    appName = routeName = (route.split('/')[0] || 'index');

    // If this isn't the error route, Find Any Custom Route Mappings
    if(!isService && !isError){
      this._currentRoute = route.split('/')[0];
      _.any(this.config.handlers, (handler) => {
        if (handler.regex.test(route)){
          appName = handler.app;
          this._currentRoute = handler.route;
          return true;
        }
      });
    }

    // Wrap these async resource fetches in a promise and return it.
    // This promise resolves when both css and js resources are loaded
    // It rejects if either of the css or js resources fails to load.
    return new Promise((resolve, reject) => {

      var throwError = (err) => {
        // If we are already in an error state, this means we were unable to load
        // a custom error page. Uninstall anything we have and insert our default 404 page.
        if(this.status === ERROR){
          if(isService) return resolve(err);
          this._uninstallResource();
          container.innerHTML = DEFAULT_404_PAGE;
          return resolve(err);
        }

        // Set our status to error and attempt to load a custom error page.
        console.error('Could not ' + ((isService) ? 'load the ' + appName + ' service:' : 'find the ' + appName + ' app.', 'at', ('/' + route)));
        this.status = ERROR;
        this._currentRoute = route;
        resolve(this._fetchResource(ERROR_ROUTE_NAME, container));
      };

      // If the values we got from installing our resources are unexpected, 404
      // Otherwise, set status, activate the css, and install the page component
      var install = (response) => {
        var cssElement = response[0], PageClass = response[1];
        if(!(cssElement instanceof Element) || typeof PageClass !== 'function') return throwError();
        (!isService && !isError) && (this.status = SUCCESS);
        cssElement && cssElement.removeAttribute('disabled');

        this._installResource(PageClass, appName, container).then(resolve, resolve);
      };

      // If loading a page, set status to loading
      (!isService && !isError) && (this.status = LOADING);

      // If Page Is Already Loaded Then The Route Does Not Exist. 404 and Exit.
      if (this.current && this.current.__name === (this.uid + '-' + appName)) return throwError();
      // Fetch our css and js in paralell, install or throw when both complete
      Promise.all([this._fetchCSS(routeName, appName), this._fetchJavascript(routeName, appName)])
      .then(install, throwError);

    });
  }
});

export default ReboundRouter;
<|endoftext|>


<commit_before>/* globals EV */

class StreamerCentral {
	constructor() {
		this.instances = {};
	}
}

Meteor.StreamerCentral = new StreamerCentral;

Meteor.Streamer = class Streamer extends EV {
	constructor(name, {retransmit = true, retransmitToSelf = false} = {}) {
		if (Meteor.StreamerCentral.instances[name]) {
			console.warn('Streamer instance already exists:', name);
			return Meteor.StreamerCentral.instances[name];
		}

		super();

		Meteor.StreamerCentral.instances[name] = this;

		this.name = name;
		this.retransmit = retransmit;
		this.retransmitToSelf = retransmitToSelf;

		this.subscriptions = [];
		this.subscriptionsByEventName = {};
		this.transformers = {};

		this.iniPublication();
		this.initMethod();

		this._allowRead = {};
		this._allowWrite = {};

		this.allowRead('none');
		this.allowWrite('none');
	}

	get name() {
		return this._name;
	}

	set name(name) {
		check(name, String);
		this._name = name;
	}

	get subscriptionName() {
		return `stream-${this.name}`;
	}

	get retransmit() {
		return this._retransmit;
	}

	set retransmit(retransmit) {
		check(retransmit, Boolean);
		this._retransmit = retransmit;
	}

	get retransmitToSelf() {
		return this._retransmitToSelf;
	}

	set retransmitToSelf(retransmitToSelf) {
		check(retransmitToSelf, Boolean);
		this._retransmitToSelf = retransmitToSelf;
	}

	allowRead(eventName, fn) {
		if (fn === undefined) {
			fn = eventName;
			eventName = '__all__';
		}

		if (typeof fn === 'function') {
			return this._allowRead[eventName] = fn;
		}

		if (fn === 'all' || fn === true) {
			return this._allowRead[eventName] = function() {return true;};
		}

		if (fn === 'none' || fn === false) {
			return this._allowRead[eventName] = function() {return false;};
		}

		if (fn === 'logged') {
			return this._allowRead[eventName] = function() {return Boolean(this.userId);};
		}
	}

	allowWrite(eventName, fn) {
		if (fn === undefined) {
			fn = eventName;
			eventName = '__all__';
		}

		if (typeof fn === 'function') {
			return this._allowWrite[eventName] = fn;
		}

		if (fn === 'all' || fn === true) {
			return this._allowWrite[eventName] = function() {return true;};
		}

		if (fn === 'none' || fn === false) {
			return this._allowWrite[eventName] = function() {return false;};
		}

		if (fn === 'logged') {
			return this._allowWrite[eventName] = function() {return Boolean(this.userId);};
		}
	}

	isReadAllowed(scope, eventName) {
		if (this._allowRead[eventName]) {
			return this._allowRead[eventName].call(scope, eventName);
		}

		return this._allowRead['__all__'].call(scope, eventName);
	}

	isWriteAllowed(scope, eventName, args) {
		if (this._allowWrite[eventName]) {
			return this._allowWrite[eventName].call(scope, eventName, ...args);
		}

		return this._allowWrite['__all__'].call(scope, eventName, ...args);
	}

	addSubscription(subscription, eventName) {
		this.subscriptions.push(subscription);

		if (!this.subscriptionsByEventName[eventName]) {
			this.subscriptionsByEventName[eventName] = [];
		}

		this.subscriptionsByEventName[eventName].push(subscription);
	}

	removeSubscription(subscription, eventName) {
		const index = this.subscriptions.indexOf(subscription);
		if (index > -1) {
			this.subscriptions.splice(index, 1);
		}

		if (this.subscriptionsByEventName[eventName]) {
			const index = this.subscriptionsByEventName[eventName].indexOf(subscription);
			if (index > -1) {
				this.subscriptionsByEventName[eventName].splice(index, 1);
			}
		}
	}

	transform(eventName, fn) {
		if (typeof eventName === 'function') {
			fn = eventName;
			eventName = '__all__';
		}

		if (!this.transformers[eventName]) {
			this.transformers[eventName] = [];
		}

		this.transformers[eventName].push(fn);
	}

	applyTransformers(methodScope, eventName, args) {
		if (this.transformers['__all__']) {
			this.transformers['__all__'].forEach((transform) => {
				args = transform.call(methodScope, eventName, args);
				methodScope.tranformed = true;
				if (!Array.isArray(args)) {
					args = [args];
				}
			});
		}

		if (this.transformers[eventName]) {
			this.transformers[eventName].forEach((transform) => {
				args = transform.call(methodScope, ...args);
				methodScope.tranformed = true;
				if (!Array.isArray(args)) {
					args = [args];
				}
			});
		}

		return args;
	}

	iniPublication() {
		const stream = this;
		Meteor.publish(this.subscriptionName, function(eventName, useCollection) {
			if (stream.isReadAllowed(this, eventName) !== true) {
				this.stop();
				return;
			}

			const subscription = {
				subscription: this,
				eventName: eventName
			};

			stream.addSubscription(subscription, eventName);

			this.onStop(() => {
				stream.removeSubscription(subscription, eventName);
			});

			if (useCollection === true) {
				// Collection compatibility
				this._session.sendAdded(stream.subscriptionName, 'id', {
					eventName: eventName
				});
			}

			this.ready();
		});
	}

	initMethod() {
		const stream = this;
		const method = {};

		method[this.subscriptionName] = function(eventName, ...args) {
			this.unblock();

			if (stream.isWriteAllowed(this, eventName, args) !== true) {
				return;
			}

			const methodScope = {
				userId: this.userId,
				connection: this.connection,
				originalParams: args,
				tranformed: false
			};

			args = stream.applyTransformers(methodScope, eventName, args);

			super.emitWithScope(eventName, methodScope, ...args);

			if (stream.retransmit === true) {
				stream._emit(eventName, args, this.connection);
			}
		};

		try {
			Meteor.methods(method);
		} catch (e) {
			console.error(e);
		}
	}

	_emit(eventName, args, origin) {
		const subscriptions = this.subscriptionsByEventName[eventName];
		if (!Array.isArray(subscriptions)) {
			return;
		}

		subscriptions.forEach((subscription) => {
			if (this.retransmitToSelf === false && origin && origin === subscription.subscription.connection) {
				return;
			}

			subscription.subscription._session.sendChanged(this.subscriptionName, 'id', {
				eventName: eventName,
				args: args
			});
		});
	}

	emit(eventName, ...args) {
		this._emit(eventName, args);
	}
};
<commit_msg>Validate shortcuts for allowRead and allowWrite<commit_after>/* globals EV */

class StreamerCentral {
	constructor() {
		this.instances = {};
	}
}

Meteor.StreamerCentral = new StreamerCentral;

Meteor.Streamer = class Streamer extends EV {
	constructor(name, {retransmit = true, retransmitToSelf = false} = {}) {
		if (Meteor.StreamerCentral.instances[name]) {
			console.warn('Streamer instance already exists:', name);
			return Meteor.StreamerCentral.instances[name];
		}

		super();

		Meteor.StreamerCentral.instances[name] = this;

		this.name = name;
		this.retransmit = retransmit;
		this.retransmitToSelf = retransmitToSelf;

		this.subscriptions = [];
		this.subscriptionsByEventName = {};
		this.transformers = {};

		this.iniPublication();
		this.initMethod();

		this._allowRead = {};
		this._allowWrite = {};

		this.allowRead('none');
		this.allowWrite('none');
	}

	get name() {
		return this._name;
	}

	set name(name) {
		check(name, String);
		this._name = name;
	}

	get subscriptionName() {
		return `stream-${this.name}`;
	}

	get retransmit() {
		return this._retransmit;
	}

	set retransmit(retransmit) {
		check(retransmit, Boolean);
		this._retransmit = retransmit;
	}

	get retransmitToSelf() {
		return this._retransmitToSelf;
	}

	set retransmitToSelf(retransmitToSelf) {
		check(retransmitToSelf, Boolean);
		this._retransmitToSelf = retransmitToSelf;
	}

	allowRead(eventName, fn) {
		if (fn === undefined) {
			fn = eventName;
			eventName = '__all__';
		}

		if (typeof fn === 'function') {
			return this._allowRead[eventName] = fn;
		}

		if (typeof fn === 'string' && ['all', 'none', 'logged'].indexOf(fn) === -1) {
			console.error(`allowRead shortcut '${fn}' is invalid`);
		}

		if (fn === 'all' || fn === true) {
			return this._allowRead[eventName] = function() {return true;};
		}

		if (fn === 'none' || fn === false) {
			return this._allowRead[eventName] = function() {return false;};
		}

		if (fn === 'logged') {
			return this._allowRead[eventName] = function() {return Boolean(this.userId);};
		}
	}

	allowWrite(eventName, fn) {
		if (fn === undefined) {
			fn = eventName;
			eventName = '__all__';
		}

		if (typeof fn === 'function') {
			return this._allowWrite[eventName] = fn;
		}

		if (typeof fn === 'string' && ['all', 'none', 'logged'].indexOf(fn) === -1) {
			console.error(`allowWrite shortcut '${fn}' is invalid`);
		}

		if (fn === 'all' || fn === true) {
			return this._allowWrite[eventName] = function() {return true;};
		}

		if (fn === 'none' || fn === false) {
			return this._allowWrite[eventName] = function() {return false;};
		}

		if (fn === 'logged') {
			return this._allowWrite[eventName] = function() {return Boolean(this.userId);};
		}
	}

	isReadAllowed(scope, eventName) {
		if (this._allowRead[eventName]) {
			return this._allowRead[eventName].call(scope, eventName);
		}

		return this._allowRead['__all__'].call(scope, eventName);
	}

	isWriteAllowed(scope, eventName, args) {
		if (this._allowWrite[eventName]) {
			return this._allowWrite[eventName].call(scope, eventName, ...args);
		}

		return this._allowWrite['__all__'].call(scope, eventName, ...args);
	}

	addSubscription(subscription, eventName) {
		this.subscriptions.push(subscription);

		if (!this.subscriptionsByEventName[eventName]) {
			this.subscriptionsByEventName[eventName] = [];
		}

		this.subscriptionsByEventName[eventName].push(subscription);
	}

	removeSubscription(subscription, eventName) {
		const index = this.subscriptions.indexOf(subscription);
		if (index > -1) {
			this.subscriptions.splice(index, 1);
		}

		if (this.subscriptionsByEventName[eventName]) {
			const index = this.subscriptionsByEventName[eventName].indexOf(subscription);
			if (index > -1) {
				this.subscriptionsByEventName[eventName].splice(index, 1);
			}
		}
	}

	transform(eventName, fn) {
		if (typeof eventName === 'function') {
			fn = eventName;
			eventName = '__all__';
		}

		if (!this.transformers[eventName]) {
			this.transformers[eventName] = [];
		}

		this.transformers[eventName].push(fn);
	}

	applyTransformers(methodScope, eventName, args) {
		if (this.transformers['__all__']) {
			this.transformers['__all__'].forEach((transform) => {
				args = transform.call(methodScope, eventName, args);
				methodScope.tranformed = true;
				if (!Array.isArray(args)) {
					args = [args];
				}
			});
		}

		if (this.transformers[eventName]) {
			this.transformers[eventName].forEach((transform) => {
				args = transform.call(methodScope, ...args);
				methodScope.tranformed = true;
				if (!Array.isArray(args)) {
					args = [args];
				}
			});
		}

		return args;
	}

	iniPublication() {
		const stream = this;
		Meteor.publish(this.subscriptionName, function(eventName, useCollection) {
			if (stream.isReadAllowed(this, eventName) !== true) {
				this.stop();
				return;
			}

			const subscription = {
				subscription: this,
				eventName: eventName
			};

			stream.addSubscription(subscription, eventName);

			this.onStop(() => {
				stream.removeSubscription(subscription, eventName);
			});

			if (useCollection === true) {
				// Collection compatibility
				this._session.sendAdded(stream.subscriptionName, 'id', {
					eventName: eventName
				});
			}

			this.ready();
		});
	}

	initMethod() {
		const stream = this;
		const method = {};

		method[this.subscriptionName] = function(eventName, ...args) {
			this.unblock();

			if (stream.isWriteAllowed(this, eventName, args) !== true) {
				return;
			}

			const methodScope = {
				userId: this.userId,
				connection: this.connection,
				originalParams: args,
				tranformed: false
			};

			args = stream.applyTransformers(methodScope, eventName, args);

			super.emitWithScope(eventName, methodScope, ...args);

			if (stream.retransmit === true) {
				stream._emit(eventName, args, this.connection);
			}
		};

		try {
			Meteor.methods(method);
		} catch (e) {
			console.error(e);
		}
	}

	_emit(eventName, args, origin) {
		const subscriptions = this.subscriptionsByEventName[eventName];
		if (!Array.isArray(subscriptions)) {
			return;
		}

		subscriptions.forEach((subscription) => {
			if (this.retransmitToSelf === false && origin && origin === subscription.subscription.connection) {
				return;
			}

			subscription.subscription._session.sendChanged(this.subscriptionName, 'id', {
				eventName: eventName,
				args: args
			});
		});
	}

	emit(eventName, ...args) {
		this._emit(eventName, args);
	}
};
<|endoftext|>


<commit_before>const Styletron = require('../styletron-client');
const test = require('tape');
const util = require('util');
const fs = require('fs');
const path = require('path');
const forEach = Array.prototype.forEach;

const fixtures = require('test-fixtures');

class StyletronTest extends Styletron {
  constructor(...args) {
    super(...args);
  }
  getCache() {
    return this.cache;
  }
  getMediaSheets() {
    return this.mediaSheets;
  }
  getuniqueCount() {
    return this.uniqueCount;
  }
}

test('hydration basic', t => {
  const elements = createFixtures([{
    css: '.c4:hover{display:none}.c0{color:red}.c1{color:green}',
  }, {
    media: '(max-width: 800px)',
    css: '.c3:hover{color:green}.c2{color:green}'
  }]);
  const instance = new StyletronTest(elements);
  t.deepEqual(instance.getCache(), fixtures.basic.cache, 'cache hydrated');
  t.equal(instance.getuniqueCount(), 5, 'count correctly hyrdated');
  const newClass = instance.injectDeclaration({prop: 'color', val: 'purple', media: '(max-width: 800px)'});
  t.equal(newClass, 'c5', 'new class with correct count');
  t.end();
});

test('rule insertion order', t => {
  const element = createStyleElement('');
  const instance = new StyletronTest([element]);
  const decls = [
    {prop: 'color', val: 'red'},
    {prop: 'color', val: 'blue'},
    {prop: 'color', val: 'blue', media: '(max-width: 333px)'},
    {prop: 'color', val: 'green'},
    {prop: 'color', val: 'red', media: 'screen and (max-width: 400px)'},
    {prop: 'color', val: 'purple'}
  ];
  decls.forEach(decl => instance.injectDeclaration(decl));
  t.equal(element.sheet.rules.length, 4);
  const mainExpected = [
    '.c0 { color: red; }',
    '.c1 { color: blue; }',
    '.c3 { color: green; }',
    '.c5 { color: purple; }',
  ];
  const mediaExpected = {
    '(max-width: 333px)': [
      '.c2 { color: blue; }'
    ],
    'screen and (max-width: 400px)': [
      '.c4 { color: red; }'
    ]
  };
  forEach.call(element.sheet.rules, (rule, i) => {
    t.equal(rule.cssText, mainExpected[i]);
  });
  const mediaSheets = instance.getMediaSheets();
  t.deepEqual(Object.keys(mediaSheets), [
    '(max-width: 333px)',
    'screen and (max-width: 400px)'
  ]);
  Object.keys(mediaExpected).forEach(mediaKey => {
    forEach.call(mediaSheets[mediaKey].sheet.rules, (rule, i) => {
      t.equal(rule.cssText, mediaExpected[mediaKey][i], 'media decl matches');
    });
  });
  t.end();
});

function createFixtures(sheets) {
  return sheets.map(sheet => createStyleElement(sheet.css, sheet.media));
}

function createStyleElement(css, media) {
  const element = document.createElement('style');
  element.appendChild(document.createTextNode(css));
  if (media) {
    element.setAttribute('media', media);
  }
  document.body.appendChild(element);
  return element;
}
<commit_msg>Minor formatting cleanup<commit_after>const Styletron = require('../styletron-client');
const test = require('tape');
const util = require('util');
const fs = require('fs');
const path = require('path');
const forEach = Array.prototype.forEach;

const fixtures = require('test-fixtures');

class StyletronTest extends Styletron {
  constructor(...args) {
    super(...args);
  }
  getCache() {
    return this.cache;
  }
  getMediaSheets() {
    return this.mediaSheets;
  }
  getUniqueDeclarationCount() {
    return this.uniqueCount;
  }
}

test('hydration basic', t => {
  const elements = createFixtures([{
    css: '.c4:hover{display:none}.c0{color:red}.c1{color:green}',
  }, {
    media: '(max-width: 800px)',
    css: '.c3:hover{color:green}.c2{color:green}'
  }]);
  const instance = new StyletronTest(elements);
  t.deepEqual(instance.getCache(), fixtures.basic.cache, 'cache hydrated');
  t.equal(instance.getUniqueDeclarationCount(), 5, 'count correctly hyrdated');
  const newClass = instance.injectDeclaration({prop: 'color', val: 'purple', media: '(max-width: 800px)'});
  t.equal(newClass, 'c5', 'new class with correct count');
  t.end();
});

test('rule insertion order', t => {
  const element = createStyleElement('');
  const instance = new StyletronTest([element]);
  const decls = [
    {prop: 'color', val: 'red'},
    {prop: 'color', val: 'blue'},
    {prop: 'color', val: 'blue', media: '(max-width: 333px)'},
    {prop: 'color', val: 'green'},
    {prop: 'color', val: 'red', media: 'screen and (max-width: 400px)'},
    {prop: 'color', val: 'purple'}
  ];
  decls.forEach(decl => instance.injectDeclaration(decl));
  t.equal(element.sheet.rules.length, 4);
  const mainExpected = [
    '.c0 { color: red; }',
    '.c1 { color: blue; }',
    '.c3 { color: green; }',
    '.c5 { color: purple; }',
  ];
  const mediaExpected = {
    '(max-width: 333px)': [
      '.c2 { color: blue; }'
    ],
    'screen and (max-width: 400px)': [
      '.c4 { color: red; }'
    ]
  };
  forEach.call(element.sheet.rules, (rule, i) => {
    t.equal(rule.cssText, mainExpected[i]);
  });
  const mediaSheets = instance.getMediaSheets();
  t.deepEqual(Object.keys(mediaSheets), [
    '(max-width: 333px)',
    'screen and (max-width: 400px)'
  ]);
  Object.keys(mediaExpected).forEach(mediaKey => {
    forEach.call(mediaSheets[mediaKey].sheet.rules, (rule, i) => {
      t.equal(rule.cssText, mediaExpected[mediaKey][i], 'media decl matches');
    });
  });
  t.end();
});

function createFixtures(sheets) {
  return sheets.map(sheet => createStyleElement(sheet.css, sheet.media));
}

function createStyleElement(css, media) {
  const element = document.createElement('style');
  element.appendChild(document.createTextNode(css));
  if (media) {
    element.setAttribute('media', media);
  }
  document.body.appendChild(element);
  return element;
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Group } from '@vx/group';
import additionalProps from '../util/additionalProps';
import { geoOrthographic, geoAlbers, geoMercator, geoPath } from 'd3-geo';

// TODO: Implement all projections of d3-geo
const projectionMapping = {
  orthographic: geoOrthographic(),
  albers: geoAlbers(),
  mercator: geoMercator()
};

/**
 * Component for all projections.
 */
export default function Projection({
  data,
  projection = 'mercator',
  projectionFunc,
  clipAngle,
  clipExtent,
  scale,
  translate,
  center,
  rotate,
  precision,
  fitExtent,
  fitSize,
  centroid,
  className,
  ...restProps
}) {
  const currProjection = projectionMapping[projection];

  if (clipAngle) currProjection.clipAngle(clipAngle);
  if (clipExtent) currProjection.clipExtent(clipExtent);
  if (scale) currProjection.scale(scale);
  if (translate) currProjection.translate(translate);
  if (center) currProjection.translate(center);
  if (rotate) currProjection.rotate(rotate);
  if (precision) currProjection.rotate(precision);
  if (fitExtent) currProjection.fitExtent(...fitExtent);
  if (fitSize) currProjection.fitSize(...fitSize);

  const path = geoPath().projection(currProjection);

  return (
    <Group className={`vx-${projection}-group`}>
      {data.map((feature, i) => {
        let c;
        if (centroid) c = path.centroid(feature);
        return (
          <g key={`${projection}-${i}`}>
            <path
              className={classnames(`vx-${projection}`, className)}
              d={path(feature)}
              {...additionalProps(restProps, {
                ...feature,
                index: i,
                centroid: c
              })}
            />
            {centroid && centroid(c, feature)}
          </g>
        );
      })}
      {/* TODO: Maybe find a different way to pass projection function to use for example invert */}
      {projectionFunc && projectionFunc(currProjection)}
    </Group>
  );
}

Projection.propTypes = {
  data: PropTypes.array.isRequired,
  projection: PropTypes.string,
  projectionFunc: PropTypes.func,
  clipAngle: PropTypes.number,
  clipExtent: PropTypes.array,
  scale: PropTypes.number,
  translate: PropTypes.array,
  center: PropTypes.array,
  rotate: PropTypes.array,
  precision: PropTypes.number,
  fitExtent: PropTypes.array,
  fitSize: PropTypes.array,
  centroid: PropTypes.func,
  className: PropTypes.string
};
<commit_msg>[geo] Using factories instead of singletons to map projections.<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Group } from '@vx/group';
import additionalProps from '../util/additionalProps';
import { geoOrthographic, geoAlbers, geoMercator, geoPath } from 'd3-geo';

// TODO: Implement all projections of d3-geo
const projectionMapping = {
  orthographic: () => geoOrthographic(),
  albers: () => geoAlbers(),
  mercator: () => geoMercator()
};

/**
 * Component for all projections.
 */
export default function Projection({
  data,
  projection = 'mercator',
  projectionFunc,
  clipAngle,
  clipExtent,
  scale,
  translate,
  center,
  rotate,
  precision,
  fitExtent,
  fitSize,
  centroid,
  className,
  ...restProps
}) {
  const currProjection = projectionMapping[projection]();

  if (clipAngle) currProjection.clipAngle(clipAngle);
  if (clipExtent) currProjection.clipExtent(clipExtent);
  if (scale) currProjection.scale(scale);
  if (translate) currProjection.translate(translate);
  if (center) currProjection.translate(center);
  if (rotate) currProjection.rotate(rotate);
  if (precision) currProjection.rotate(precision);
  if (fitExtent) currProjection.fitExtent(...fitExtent);
  if (fitSize) currProjection.fitSize(...fitSize);

  const path = geoPath().projection(currProjection);

  return (
    <Group className={`vx-${projection}-group`}>
      {data.map((feature, i) => {
        let c;
        if (centroid) c = path.centroid(feature);
        return (
          <g key={`${projection}-${i}`}>
            <path
              className={classnames(`vx-${projection}`, className)}
              d={path(feature)}
              {...additionalProps(restProps, {
                ...feature,
                index: i,
                centroid: c
              })}
            />
            {centroid && centroid(c, feature)}
          </g>
        );
      })}
      {/* TODO: Maybe find a different way to pass projection function to use for example invert */}
      {projectionFunc && projectionFunc(currProjection)}
    </Group>
  );
}

Projection.propTypes = {
  data: PropTypes.array.isRequired,
  projection: PropTypes.string,
  projectionFunc: PropTypes.func,
  clipAngle: PropTypes.number,
  clipExtent: PropTypes.array,
  scale: PropTypes.number,
  translate: PropTypes.array,
  center: PropTypes.array,
  rotate: PropTypes.array,
  precision: PropTypes.number,
  fitExtent: PropTypes.array,
  fitSize: PropTypes.array,
  centroid: PropTypes.func,
  className: PropTypes.string
};
<|endoftext|>


<commit_before>import R from 'ramda';
import { assert } from 'chai';

import {
  tapP,
  retryOrFail,
} from '../src/promises';

describe('promises', () => {
  describe('tapP()', () => {
    it('should return the same value', () => {
      const promiseFn = () => new Promise((resolve) => {
        setTimeout(() => resolve(true), 5);
      });

      Promise.resolve(1)
        .then(tapP(promiseFn))
        .then(value => assert.equal(value, 1));
    });
    it('should pass argument into promiseFn', () => {
      const promiseFn = arg => new Promise((resolve) => {
        const newValue = arg + 5;
        assert.equal(newValue, 6);
        setTimeout(() => resolve(newValue), 5);
      });

      Promise.resolve(1)
        .then(tapP(promiseFn));
    });
    it('should return Promise.reject if inner function return Promise.reject', () => {
      const promiseFn = () => Promise.reject('reject');

      Promise.resolve(1)
        .then(tapP(promiseFn))
        .catch(out => assert.equal(out, 'reject'));
    });
  });

  describe('retryOrFail()', () => {
    it('should keep retrying until there are no tries left', () => {
      const retriesSchedule = [1, 1, 1, 1];
      let retriesCount = 0;

      const result = retryOrFail(
        retriesSchedule,
        R.F, // never fail by predicate
        R.identity, // do not transform rejected value
        () => {
          retriesCount += 1;
          return Promise.reject(retriesCount);
        },
      )();

      return result.then(() => {
        assert.fail('got resolved promise', 'expected rejected promise');
      }).catch(() => {
        assert.equal(
          retriesCount,
          retriesSchedule.length
        );
      });
    });
    it('should stop retrying if `stopFn` predicate returns true', () => {
      const retriesSchedule = [1, 1, 1, 1];
      let retriesCount = 0;

      const retryToStopOn = 2;

      const result = retryOrFail(
        retriesSchedule,
        () => retriesCount >= retryToStopOn,
        R.identity,
        () => {
          retriesCount += 1;
          return Promise.reject(retriesCount);
        },
      )();

      return result.then(() => {
        assert.fail('got resolved promise', 'expected rejected promise');
      }).catch(() => {
        assert.equal(retriesCount, retryToStopOn);
      });
    });
    it('should stop retrying after a successful try', () => {
      const retriesSchedule = [1, 1, 1, 1];
      let retriesCount = 0;

      const retryToSucceedOn = 2;

      const result = retryOrFail(
        retriesSchedule,
        R.F,
        R.identity,
        () => {
          retriesCount += 1;
          return retriesCount === retryToSucceedOn
            ? Promise.resolve(retriesCount)
            : Promise.reject(retriesCount);
        },
      )();

      return result.then(() => {
        assert.equal(retriesCount, retryToSucceedOn);
      });
    });
    it('should wait specified amount of time before attempting a next try', () => {
      const retriesSchedule = [15, 20, 30, 35];
      let retriesCount = 0;

      const beginTime = Date.now();
      let lastTryTime = beginTime;

      const result = retryOrFail(
        retriesSchedule,
        R.F,
        R.identity,
        () => {
          const now = Date.now();

          assert.isAtLeast(
            now - lastTryTime,
            retriesSchedule[retriesCount]
          );

          retriesCount += 1;
          lastTryTime = now;
          return Promise.reject(retriesCount);
        },
      )();

      return result.then(() => {
        assert.fail('got resolved promise', 'expected rejected promise');
      }).catch(() => {
        assert.equal(retriesCount, retriesSchedule.length);
        assert.isAtLeast(
          lastTryTime - beginTime,
          R.sum(retriesSchedule)
        );
      });
    });
  });
});
<commit_msg>test(xod-func-tools): remove test for `retryOrFail` about waiting<commit_after>import R from 'ramda';
import { assert } from 'chai';

import {
  tapP,
  retryOrFail,
} from '../src/promises';

describe('promises', () => {
  describe('tapP()', () => {
    it('should return the same value', () => {
      const promiseFn = () => new Promise((resolve) => {
        setTimeout(() => resolve(true), 5);
      });

      Promise.resolve(1)
        .then(tapP(promiseFn))
        .then(value => assert.equal(value, 1));
    });
    it('should pass argument into promiseFn', () => {
      const promiseFn = arg => new Promise((resolve) => {
        const newValue = arg + 5;
        assert.equal(newValue, 6);
        setTimeout(() => resolve(newValue), 5);
      });

      Promise.resolve(1)
        .then(tapP(promiseFn));
    });
    it('should return Promise.reject if inner function return Promise.reject', () => {
      const promiseFn = () => Promise.reject('reject');

      Promise.resolve(1)
        .then(tapP(promiseFn))
        .catch(out => assert.equal(out, 'reject'));
    });
  });

  describe('retryOrFail()', () => {
    it('should keep retrying until there are no tries left', () => {
      const retriesSchedule = [1, 1, 1, 1];
      let retriesCount = 0;

      const result = retryOrFail(
        retriesSchedule,
        R.F, // never fail by predicate
        R.identity, // do not transform rejected value
        () => {
          retriesCount += 1;
          return Promise.reject(retriesCount);
        },
      )();

      return result.then(() => {
        assert.fail('got resolved promise', 'expected rejected promise');
      }).catch(() => {
        assert.equal(
          retriesCount,
          retriesSchedule.length
        );
      });
    });
    it('should stop retrying if `stopFn` predicate returns true', () => {
      const retriesSchedule = [1, 1, 1, 1];
      let retriesCount = 0;

      const retryToStopOn = 2;

      const result = retryOrFail(
        retriesSchedule,
        () => retriesCount >= retryToStopOn,
        R.identity,
        () => {
          retriesCount += 1;
          return Promise.reject(retriesCount);
        },
      )();

      return result.then(() => {
        assert.fail('got resolved promise', 'expected rejected promise');
      }).catch(() => {
        assert.equal(retriesCount, retryToStopOn);
      });
    });
    it('should stop retrying after a successful try', () => {
      const retriesSchedule = [1, 1, 1, 1];
      let retriesCount = 0;

      const retryToSucceedOn = 2;

      const result = retryOrFail(
        retriesSchedule,
        R.F,
        R.identity,
        () => {
          retriesCount += 1;
          return retriesCount === retryToSucceedOn
            ? Promise.resolve(retriesCount)
            : Promise.reject(retriesCount);
        },
      )();

      return result.then(() => {
        assert.equal(retriesCount, retryToSucceedOn);
      });
    });
  });
});
<|endoftext|>


<commit_before>import Popper from 'popper.js';
import Tooltip from 'tooltip.js';

export default {

    props: {

        // The name of the input. Will also be the name of the value, when the form gets submitted.
        name: {
            type: String,
            required: true
        },

        // The predefined value of the input.
        // See data: 'submitValue'
        value: {
            type: String,
            default: ''
        },

        // The language key of the label.
        // See computed data: 'label'.
        langKey: {
            type: String
        },

        // True, if the input shall be disabled.
        disabled: {
            type: Boolean,
            default: false
        },

        // True, if a label shall be displayed above the input.
        showLabel: {
            type: Boolean,
            default: true
        },

        // Function to check if the input is valid. If it is invalid an error message,
        // based upon the property 'name' and 'langKey' will be shown to the user.
        // The function receives the current value of the input as first parameter
        // and a callback function as the second. This callback must return true,
        // if the input is valid and false otherwise.
        check: {
            type: Function
        },

        // States if a value on the input is required.
        required: {
            type: Boolean,
            default: false
        },

        // States if a popover shall be shown next to the input when an error occurred.
        popover: {
            type: Boolean,
            default: true
        },

        // The position of the popover when the 'popover' prop is set to 'true'
        popoverPosition: {
            type: String,
            default: 'right'
        },

        // The help text to show as a tooltip when hovering over the input
        help: null,

        // The position of the popover when the 'help' prop is set
        helpPosition: {
            type: String,
            default: 'right'
        },

        // The specific color of the input group
        color: null,

        // The specific size of the input group
        size: null,

        // States if errors on the input shall be ignored
        ignoreErrors: {
            type: Boolean,
            default: false
        }
    },

    data: function () {
        return {
            submitValue: '',

            labelMessage: '',

            // States if the input's value is valid.
            valid: !this.required || this.value,

            // States if the input's value is invalid.
            invalid: false,

            // The parent components of the component.
            parents: '',

            // States if the input is currently focused.
            active: false,

            // The currently shown popover
            currentPopover: null,
        }
    },

    computed: {

        // The label text of the input, based upon the property 'name' or the property 'langKey', if it is set.
        label: function () {
            let langKey = this.name;
            if (this.langKey) {
                langKey = this.langKey + '.' + this.name;
            }
            langKey = 'input.' + langKey;
            let label = this.$t(langKey);

            if (label === langKey) {
                label = titleCase(this.name);
            }

            if (this.required) {
                label += ' *';
            }
            return label;
        },

        // The text to show to the user, if the check function exists and returns false.
        errorMessage: function () {
            return this.getLocalizationString(this.name);
        },

        // The text to show to the user, if the input is required and the user did not enter a value.
        requiredMessage: function () {
            return this.getLocalizationString('required', {'attribute': this.name});
        },

        // The name of the input. Will also be the name of the value, when the form gets submitted.
        // Info: This value is based upon the 'name' property.
        submitName: function () {
            return this.name;
        },

        // States if the content has changed since the page load
        contentChanged: function () {
            return this.submitValue !== this.value;
        },

        // The element that activates the help tooltip on hover
        tooltipActivator: function() {
            return this.$refs.inputWrapper;
        }
    },

    mounted: function () {
        this.$nextTick(function () {
            this.submitValue = this.value;
            this.parents = getListOfParents(this);

            if (this.help) {
                new Tooltip(this.tooltipActivator, {
                    placement: this.helpPosition,
                    title: this.help
                });
            }

        })
    },

    watch: {
        submitValue: function (val) {

            // Convert booleans to integer
            if (typeof(val) === "boolean") {
                this.submitValue = val ? 1 : 0;
                return;
            }

            for (let index in this.parents) {
                let parent = this.parents[index];
                if (parent.hasOwnProperty("form")) {
                    parent.form[this.submitName] = val;
                }
            }

            window.eventHub.$emit(this.name + '-input-changed', val);

            // Only check input if the input wasn't cleared
            if (val || this.active) {
                this.checkInput();
                this.validateParentForm();
            }

        },

        value: function (val) {
            this.submitValue = val;
        },

        labelMessage: function (message, oldMessage) {
            if (message !== oldMessage && this.currentPopover) {
                this.currentPopover.destroy();
                this.currentPopover = null;
            }

            if (message && this.popover && this.$refs.inputWrapper) {
                let popover = $('

' + message + '

');
                $(this.$refs.inputWrapper).append(popover);
                this.currentPopover = new Popper(this.$refs.inputWrapper, popover[0], {
                    placement: this.popoverPosition,
                    removeOnDestroy: true,
                    modifiers: {
                        flip: { behavior: ['bottom'] }
                    }
                });
            }
        }
    },

    methods: {

        /**
         * Activates the inputs editing style.
         */
        activate: function () {
            $(this.$refs.inputWrapper).addClass('active');
            this.active = true;
        },

        /**
         * Deactivates the inputs editing style.
         */
        deactivate: function () {
            $(this.$refs.inputWrapper).removeClass('active');
            this.active = false;
        },

        /**
         * Checks if the current value of the input is valid and
         * updates the input's label, based on the input's value.
         */
        checkInput: function () {
            if (this.checkRequired()
                && this.checkComponentSpecific()) {
                if (isFunction(this.check)) {
                    this.check(this.submitValue, (valid, errorMessage) => {
                        if (valid) {
                            this.addSuccess();
                        } else {
                            this.addError(errorMessage);
                        }
                    });
                } else {
                    this.addSuccess();
                }
            }
        },

        validateParentForm: function () {
            for (let index in this.parents) {
                let parent = this.parents[index];
                if (isFunction(parent.updateFormSubmitPermission)) {
                    parent.updateFormSubmitPermission();
                }
            }
        },

        /**
         * Checks if the input's value is valid regarding the property 'required'.
         * If not an error message will be shown on the input.
         *
         * @returns {boolean}
         */
        checkRequired: function () {
            if (!this.submitValue && this.required) {
                this.addError(this.requiredMessage, true);

                return false;
            }
            return true;
        },

        /**
         * Checks if the input's value is valid regarding the specific needs in an input component.
         *
         * @returns {boolean}
         */
        checkComponentSpecific: function () {
            return true;
        },

        /**
         * Adds the specified error message to the input field.
         *
         * @param errorMessage
         * @param forceError
         */
        addError: function (errorMessage = this.errorMessage, forceError = false) {
            if (forceError || !this.ignoreErrors) {
                this.labelMessage = errorMessage;
                this.invalid = true;
                this.valid = false;
            }
        },

        /**
         * Shows a success sign on the input field.
         */
        addSuccess: function () {
            this.labelMessage = null;
            this.invalid = false;
            this.valid = true;
        },

        /**
         * Opens the help page for the input.
         */
        openHelp: function () {
            if (this.helpPath) {
                window.open(this.helpPath, '_blank');
            }
        },

        /**
         * Resets the input's value.
         */
        reset: function () {
            this.submitValue = this.value;
            this.labelMessage = null;
            this.invalid = false;
            this.valid = !this.required || this.value;
        },

        /**
         * Clears the input's value.
         */
        clear: function () {
            this.submitValue = '';
            this.labelMessage = null;
            this.invalid = false;
            this.valid = !this.required;
        },

        /**
         * Gets the localization string for the error messages.
         *
         * @param type
         * @param props
         * @param plain
         * @returns {string}
         */
        getLocalizationString: function (type = null, props = null, plain = false) {
            let langKey = '';
            if (this.langKey && !plain) {
                langKey = this.langKey + '.';
            }

            let key = 'validation.' + langKey;
            let defaultKey = key;
            if (type) {
                key = 'validation.' + langKey + type;
                defaultKey = 'validation.' + type;
            }

            // Validate props
            if (props !== null && props.hasOwnProperty('attribute')) {
                let attribute = props.attribute;
                let attributeLangKey = 'validation.attributes.' + attribute;
                let attributeReplacement = this.$t(attributeLangKey);
                props.attribute = attributeReplacement === attributeLangKey ? attribute : attributeReplacement;
            }

            let text = this.$t(key, props);
            if (text === key) {
                text = this.$t(defaultKey, props);
            }

            return text;
        },
    }
};

<commit_msg>Added emit input changed event for the use of v-model<commit_after>import Popper from 'popper.js';
import Tooltip from 'tooltip.js';

export default {

    props: {

        // The name of the input. Will also be the name of the value, when the form gets submitted.
        name: {
            type: String,
            required: true
        },

        // The predefined value of the input.
        // See data: 'submitValue'
        value: {
            type: String,
            default: ''
        },

        // The language key of the label.
        // See computed data: 'label'.
        langKey: {
            type: String
        },

        // True, if the input shall be disabled.
        disabled: {
            type: Boolean,
            default: false
        },

        // True, if a label shall be displayed above the input.
        showLabel: {
            type: Boolean,
            default: true
        },

        // Function to check if the input is valid. If it is invalid an error message,
        // based upon the property 'name' and 'langKey' will be shown to the user.
        // The function receives the current value of the input as first parameter
        // and a callback function as the second. This callback must return true,
        // if the input is valid and false otherwise.
        check: {
            type: Function
        },

        // States if a value on the input is required.
        required: {
            type: Boolean,
            default: false
        },

        // States if a popover shall be shown next to the input when an error occurred.
        popover: {
            type: Boolean,
            default: true
        },

        // The position of the popover when the 'popover' prop is set to 'true'
        popoverPosition: {
            type: String,
            default: 'right'
        },

        // The help text to show as a tooltip when hovering over the input
        help: null,

        // The position of the popover when the 'help' prop is set
        helpPosition: {
            type: String,
            default: 'right'
        },

        // The specific color of the input group
        color: null,

        // The specific size of the input group
        size: null,

        // States if errors on the input shall be ignored
        ignoreErrors: {
            type: Boolean,
            default: false
        }
    },

    data: function () {
        return {
            submitValue: '',

            labelMessage: '',

            // States if the input's value is valid.
            valid: !this.required || this.value,

            // States if the input's value is invalid.
            invalid: false,

            // The parent components of the component.
            parents: '',

            // States if the input is currently focused.
            active: false,

            // The currently shown popover
            currentPopover: null,
        }
    },

    computed: {

        // The label text of the input, based upon the property 'name' or the property 'langKey', if it is set.
        label: function () {
            let langKey = this.name;
            if (this.langKey) {
                langKey = this.langKey + '.' + this.name;
            }
            langKey = 'input.' + langKey;
            let label = this.$t(langKey);

            if (label === langKey) {
                label = titleCase(this.name);
            }

            if (this.required) {
                label += ' *';
            }
            return label;
        },

        // The text to show to the user, if the check function exists and returns false.
        errorMessage: function () {
            return this.getLocalizationString(this.name);
        },

        // The text to show to the user, if the input is required and the user did not enter a value.
        requiredMessage: function () {
            return this.getLocalizationString('required', {'attribute': this.name});
        },

        // The name of the input. Will also be the name of the value, when the form gets submitted.
        // Info: This value is based upon the 'name' property.
        submitName: function () {
            return this.name;
        },

        // States if the content has changed since the page load
        contentChanged: function () {
            return this.submitValue !== this.value;
        },

        // The element that activates the help tooltip on hover
        tooltipActivator: function() {
            return this.$refs.inputWrapper;
        }
    },

    mounted: function () {
        this.$nextTick(function () {
            this.submitValue = this.value;
            this.parents = getListOfParents(this);

            if (this.help) {
                new Tooltip(this.tooltipActivator, {
                    placement: this.helpPosition,
                    title: this.help
                });
            }

        })
    },

    watch: {
        submitValue: function (val) {

            // Convert booleans to integer
            if (typeof(val) === "boolean") {
                this.submitValue = val ? 1 : 0;
                return;
            }

            for (let index in this.parents) {
                let parent = this.parents[index];
                if (parent.hasOwnProperty("form")) {
                    parent.form[this.submitName] = val;
                }
            }

            window.eventHub.$emit(this.name + '-input-changed', val);
            this.$emit('input', val);

            // Only check input if the input wasn't cleared
            if (val || this.active) {
                this.checkInput();
                this.validateParentForm();
            }

        },

        value: function (val) {
            this.submitValue = val;
        },

        labelMessage: function (message, oldMessage) {
            if (message !== oldMessage && this.currentPopover) {
                this.currentPopover.destroy();
                this.currentPopover = null;
            }

            if (message && this.popover && this.$refs.inputWrapper) {
                let popover = $('

' + message + '

');
                $(this.$refs.inputWrapper).append(popover);
                this.currentPopover = new Popper(this.$refs.inputWrapper, popover[0], {
                    placement: this.popoverPosition,
                    removeOnDestroy: true,
                    modifiers: {
                        flip: { behavior: ['bottom'] }
                    }
                });
            }
        }
    },

    methods: {

        /**
         * Activates the inputs editing style.
         */
        activate: function () {
            $(this.$refs.inputWrapper).addClass('active');
            this.active = true;
        },

        /**
         * Deactivates the inputs editing style.
         */
        deactivate: function () {
            $(this.$refs.inputWrapper).removeClass('active');
            this.active = false;
        },

        /**
         * Checks if the current value of the input is valid and
         * updates the input's label, based on the input's value.
         */
        checkInput: function () {
            if (this.checkRequired()
                && this.checkComponentSpecific()) {
                if (isFunction(this.check)) {
                    this.check(this.submitValue, (valid, errorMessage) => {
                        if (valid) {
                            this.addSuccess();
                        } else {
                            this.addError(errorMessage);
                        }
                    });
                } else {
                    this.addSuccess();
                }
            }
        },

        validateParentForm: function () {
            for (let index in this.parents) {
                let parent = this.parents[index];
                if (isFunction(parent.updateFormSubmitPermission)) {
                    parent.updateFormSubmitPermission();
                }
            }
        },

        /**
         * Checks if the input's value is valid regarding the property 'required'.
         * If not an error message will be shown on the input.
         *
         * @returns {boolean}
         */
        checkRequired: function () {
            if (!this.submitValue && this.required) {
                this.addError(this.requiredMessage, true);

                return false;
            }
            return true;
        },

        /**
         * Checks if the input's value is valid regarding the specific needs in an input component.
         *
         * @returns {boolean}
         */
        checkComponentSpecific: function () {
            return true;
        },

        /**
         * Adds the specified error message to the input field.
         *
         * @param errorMessage
         * @param forceError
         */
        addError: function (errorMessage = this.errorMessage, forceError = false) {
            if (forceError || !this.ignoreErrors) {
                this.labelMessage = errorMessage;
                this.invalid = true;
                this.valid = false;
            }
        },

        /**
         * Shows a success sign on the input field.
         */
        addSuccess: function () {
            this.labelMessage = null;
            this.invalid = false;
            this.valid = true;
        },

        /**
         * Opens the help page for the input.
         */
        openHelp: function () {
            if (this.helpPath) {
                window.open(this.helpPath, '_blank');
            }
        },

        /**
         * Resets the input's value.
         */
        reset: function () {
            this.submitValue = this.value;
            this.labelMessage = null;
            this.invalid = false;
            this.valid = !this.required || this.value;
        },

        /**
         * Clears the input's value.
         */
        clear: function () {
            this.submitValue = '';
            this.labelMessage = null;
            this.invalid = false;
            this.valid = !this.required;
        },

        /**
         * Gets the localization string for the error messages.
         *
         * @param type
         * @param props
         * @param plain
         * @returns {string}
         */
        getLocalizationString: function (type = null, props = null, plain = false) {
            let langKey = '';
            if (this.langKey && !plain) {
                langKey = this.langKey + '.';
            }

            let key = 'validation.' + langKey;
            let defaultKey = key;
            if (type) {
                key = 'validation.' + langKey + type;
                defaultKey = 'validation.' + type;
            }

            // Validate props
            if (props !== null && props.hasOwnProperty('attribute')) {
                let attribute = props.attribute;
                let attributeLangKey = 'validation.attributes.' + attribute;
                let attributeReplacement = this.$t(attributeLangKey);
                props.attribute = attributeReplacement === attributeLangKey ? attribute : attributeReplacement;
            }

            let text = this.$t(key, props);
            if (text === key) {
                text = this.$t(defaultKey, props);
            }

            return text;
        },
    }
};

<|endoftext|>