<commit_before>'use strict';

const Events = require('events');
const Flatten = require('lodash.flatten');
const Triton = require('triton');
const VAsync = require('vasync');
const Wreck = require('wreck');
const CIDRMatcher = require('cidr-matcher');
const ForceArray = require('force-array');
const Get = require('lodash.get');
const Uniq = require('lodash.uniq');
const Hasha = require('hasha');
const ParamCase = require('param-case');
const Queue = require('./queue');

module.exports = class ContainerPilotWatcher extends Events {
  constructor (options) {
    super();

    options = options || {};

    // todo assert options
    this._data = options.data;
    this._frequency = options.frequency || 1000;

    const TritonClient = options.Triton || Triton;

    TritonClient.createClient({
      profile: {
        url: options.url || process.env.SDC_URL,
        account: options.account || process.env.SDC_ACCOUNT,
        keyId: options.keyId || process.env.SDC_KEY_ID
      }
    }, (err, client) => {
      if (err) {
        this.emit('error', err);
        return;
      }

      this._triton = client.cloudapi;
    });
  }

  poll () {
    if (this._timeoutId) {
      return;
    }

    this._timeoutId = setTimeout(() => {
      this.check((err) => {
        if (err) {
          this.emit('error', err);
        }

        delete this._timeoutId;
        this.poll();
      });
    }, this._frequency);
  }

  _getDeploymentGroups (cb) {
    const getInstances = (service, next) => {
      service.instances({}, (err, instances) => {
        if (err) {
          return next(err);
        }

        next(null, Object.assign({}, service, {
          instances
        }));
      });
    };

    const getServices = (deploymentGroup, next) => {
      deploymentGroup.services({}, (err, services) => {
        if (err) {
          return next(err);
        }

        if (!services || !services.length) {
          return next();
        }

        VAsync.forEachParallel({
          inputs: services,
          func: getInstances
        }, (err, result) => {
          if (err) {
            return next(err);
          }

          next(null, Object.assign({}, deploymentGroup, {
            services: result.successes
          }));
        });
      });
    };

    const handleDeploymentGroups = (err, deploymentGroups) => {
      if (err) {
        return cb(err);
      }

      VAsync.forEachParallel({
        inputs: deploymentGroups,
        func: getServices
      }, (err, result) => {
        cb(err, result.successes);
      });
    };

    const getDeploymentGroups = (err, portal) => {
      if (err) {
        return cb(err);
      }

      portal.deploymentGroups({}, handleDeploymentGroups);
    };

    this._data.getPortal({}, getDeploymentGroups);
  }

  _getNetworks (networkIds = [], cb) {
    VAsync.forEachParallel({
      inputs: networkIds,
      func: (id, next) => {
        this._triton.getNetwork(id, next);
      }
    }, (err, results) => {
      cb(err, ForceArray((results || {}).successes));
    });
  }

  _getPublicIps (machine, cb) {
    this._getNetworks(machine.networks, (err, networks) => {
      if (err) {
        return cb(err);
      }

      const privateNetworkSubnets = networks
        .filter((network) => {
          return !network['public'];
        })
        .map((network) => {
          return network.subnet;
        })
        .filter(Boolean);

      const cidr = new CIDRMatcher(privateNetworkSubnets);

      const nonPrivateIps = machine.ips.filter((ip) => {
        return !cidr.contains(ip);
      });

      cb(null, nonPrivateIps);
    });
  }

  _fetchInstanceStatus (instance, cb) {
    const { machineId } = instance;

    const handleStatuses = (err, results) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      const statuses = ForceArray((results || {}).successes);

      const status = statuses.filter(Boolean).shift();

      if (!status) {
        return cb(null, instance);
      }

      const services = ForceArray(status.Services).filter(({ Name }) => {
        return Name !== 'containerpilot';
      });

      cb(null, Object.assign({}, instance, {
        cp: Object.assign({}, status, {
          Services: services
        })
      }));
    };

    const fetchStatus = (ip, next) => {
      Wreck.get(`http://${ip}:9090/status`, {
        timeout: 1000 // 1s
      }, (err, res, payload) => {
        if (err) {
          this.emit('error', err);
          return next();
        }

        if (Buffer.isBuffer(payload)) {
          payload = payload.toString();
        }

        try {
          const status = JSON.parse(payload);
          next(null, status);
        } catch (err) {
          next();
        }
      });
    };

    const handlePublicIps = (err, ips) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      VAsync.forEachParallel({
        inputs: ips,
        func: fetchStatus
      }, handleStatuses);
    };

    this._triton.getMachine(machineId, (err, machine) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      this._getPublicIps(machine, handlePublicIps);
    });
  }

  _fetchServiceStatus (service, cb) {
    VAsync.forEachParallel({
      inputs: service.instances,
      func: (instance, next) => {
        this._fetchInstanceStatus(instance, next);
      }
    }, (err, results) => {
      if (err) {
        this.emit('error', err);
      }

      cb(null, Object.assign({}, service, {
        instances: ForceArray((results || {}).successes)
      }));
    });
  }

  _fetchDeploymentGroupStatus (dg, cb) {
    if (!dg || !dg.services || !dg.services.length) {
      return cb();
    }

    VAsync.forEachParallel({
      inputs: dg.services,
      func: (service, next) => {
        this._fetchServiceStatus(service, next);
      }
    }, (err, results) => {
      if (err) {
        this.emit('error', err);
      }

      cb(null, Object.assign({}, dg, {
        services: ForceArray((results || {}).successes)
      }));
    });
  }

  _saveInstance ({ id, healthy, watches, jobs }, cb) {
    if (!id) {
      return cb();
    }

    this._data.updateInstance({
      id,
      healthy,
      watches,
      jobs
    }, cb);
  }

  _saveService ({ id, instances, connections, branches }, cb) {
    if (!id) {
      return cb();
    }

    VAsync.forEachParallel({
      inputs: instances,
      func: (instance, next) => {
        this._saveInstance(instance, next);
      }
    }, (err) => {
      if (err) {
        return cb(err);
      }

      this._data.updateService({
        id,
        connections,
        branches
      }, cb);
    });
  }

  _saveDeploymentGroup (dg, cb) {
    VAsync.forEachParallel({
      inputs: dg.services,
      func: (service, next) => {
        this._saveService(service, next);
      }
    }, cb);
  }

  static _resolveServiceConnections ({ services, service }) {
    const watches = Uniq(
      Flatten(
        ForceArray(service.instances).map(({ watches }) => {
          return watches;
        })
      )
    );

    return watches
      .map((jobName) => {
        return services.reduce((serviceId, service) => {
          if (serviceId) {
            return serviceId;
          }

          const thisServiceJobs = Uniq(
            Flatten(
              ForceArray(service.instances).map(({ jobs }) => {
                return jobs;
              })
            )
          );

          if (thisServiceJobs.indexOf(jobName) >= 0) {
            return service.id;
          }

          return serviceId;
        }, null);
      })
      .filter(Boolean)
      .filter((serviceId) => {
        return service.id !== serviceId;
      });
  }

  static _resolveInstanceHealth ({ name }, instance) {
    if (!instance) {
      return 'UNAVAILABLE';
    }

    if (!instance.cp) {
      return 'UNAVAILABLE';
    }

    const jobNames = Get(instance, 'cp.Services');

    const serviceJobs = jobNames.filter(({ Name }) => {
      return Name === name;
    });

    if (serviceJobs.length) {
      return serviceJobs.shift().Status.toUpperCase();
    }

    const almostJobNameRegexp = new RegExp(`${name}-.*`);
    const almostServiceJobs = jobNames.filter((n) => {
      return almostJobNameRegexp.test(n);
    });

    if (almostServiceJobs.length) {
      return almostServiceJobs.shift().Status.toUpperCase();
    }

    return 'UNKNOWN';
  }

  static _resolveServiceBranches ({ name, slug, instances }) {
    if (instances.length <= 1) {
      return [];
    }

    const deviantJobNames = Uniq(Flatten(instances.map(({ jobs }) => {
      return Flatten(jobs.filter((jobName) => {
        return new RegExp(`${name}-.*`).test(jobName);
      }));
    })));

    if (!deviantJobNames || !deviantJobNames.length) {
      return [];
    }

    const defaultBranch = instances.reduce((service, { id, jobs }) => {
      if (jobs.indexOf(name) >= 0) {
        return Object.assign(service, {
          instances: service.instances.concat(id)
        });
      }

      return service;
    }, {
      name: name,
      slug: slug,
      instances: []
    });

    defaultBranch.id = Hasha(JSON.stringify(defaultBranch));

    const branches = instances.reduce((branches, { id, jobs }) => {
      if (defaultBranch.instances.indexOf(id) >= 0) {
        return branches;
      }

      const branchName = jobs
        .filter((jobName) => {
          return deviantJobNames.indexOf(jobName) >= 0;
        })
        .shift();

      if (!branches[branchName]) {
        branches[branchName] = {
          name: branchName,
          slug: ParamCase(branchName),
          instances: []
        };

        branches[branchName].id = Hasha(JSON.stringify(branches[branchName]));
      }

      branches[branchName].instances.push(id);

      return branches;
    }, {});

    return Object.values(branches).concat(defaultBranch);
  }

  static _resolveDeploymentGroups (dgs) {
    return dgs
      .filter(Boolean)
      .map((dg) => {
        return Object.assign({}, dg, {
          services: ForceArray(dg.services).map((service) => {
            return Object.assign({}, service, {
              instances: ForceArray(service.instances).map((instance) => {
                const watches = Get(instance, 'cp.Watches', []);
                const jobs = Get(instance, 'cp.Services', []).map(({ Name }) => {
                  return Name;
                });

                return Object.assign({}, instance, {
                  healthy: ContainerPilotWatcher._resolveInstanceHealth(service, instance),
                  jobs,
                  watches
                });
              })
            });
          })
        });
      })
      .map((dg) => {
        return Object.assign({}, dg, {
          services: ForceArray(dg.services).map((service) => {
            return Object.assign({}, service, {
              branches: ContainerPilotWatcher._resolveServiceBranches(service),
              connections: ContainerPilotWatcher._resolveServiceConnections({
                services: dg.services,
                service
              })
            });
          })
        });
      });
  }

  check (cb) {
    if (!this._triton) {
      return cb();
    }

    const handleStatuses = (err, results) => {
      if (err) {
        this.emit('error', err);
      }

      const dgs = ContainerPilotWatcher._resolveDeploymentGroups(
        ForceArray((results || {}).successes)
      );

      VAsync.forEachParallel({
        inputs: dgs,
        func: (dg, next) => {
          Queue(dg.id, () => {
            return new Promise((resolve) => {
              this._saveDeploymentGroup(dg, (err) => {
                resolve();
                next(err);
              });
            });
          });
        }
      }, (err) => {
        if (err) {
          this.emit('error', err);
        }

        cb(null, dgs);
      });
    };

    const fetchStatuses = (err, dgs) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      VAsync.forEachParallel({
        inputs: dgs,
        func: (dg, next) => {
          this._fetchDeploymentGroupStatus(dg, next);
        }
      }, handleStatuses);
    };

    this._getDeploymentGroups(fetchStatuses);
  }
};

<commit_msg>fix(portal-api): fix branch health resolution<commit_after>'use strict';

const Events = require('events');
const Flatten = require('lodash.flatten');
const Triton = require('triton');
const VAsync = require('vasync');
const Wreck = require('wreck');
const CIDRMatcher = require('cidr-matcher');
const ForceArray = require('force-array');
const Get = require('lodash.get');
const Uniq = require('lodash.uniq');
const Hasha = require('hasha');
const ParamCase = require('param-case');
const Queue = require('./queue');

module.exports = class ContainerPilotWatcher extends Events {
  constructor (options) {
    super();

    options = options || {};

    // todo assert options
    this._data = options.data;
    this._frequency = options.frequency || 1000;

    const TritonClient = options.Triton || Triton;

    TritonClient.createClient({
      profile: {
        url: options.url || process.env.SDC_URL,
        account: options.account || process.env.SDC_ACCOUNT,
        keyId: options.keyId || process.env.SDC_KEY_ID
      }
    }, (err, client) => {
      if (err) {
        this.emit('error', err);
        return;
      }

      this._triton = client.cloudapi;
    });
  }

  poll () {
    if (this._timeoutId) {
      return;
    }

    this._timeoutId = setTimeout(() => {
      this.check((err) => {
        if (err) {
          this.emit('error', err);
        }

        delete this._timeoutId;
        this.poll();
      });
    }, this._frequency);
  }

  _getDeploymentGroups (cb) {
    const getInstances = (service, next) => {
      service.instances({}, (err, instances) => {
        if (err) {
          return next(err);
        }

        next(null, Object.assign({}, service, {
          instances
        }));
      });
    };

    const getServices = (deploymentGroup, next) => {
      deploymentGroup.services({}, (err, services) => {
        if (err) {
          return next(err);
        }

        if (!services || !services.length) {
          return next();
        }

        VAsync.forEachParallel({
          inputs: services,
          func: getInstances
        }, (err, result) => {
          if (err) {
            return next(err);
          }

          next(null, Object.assign({}, deploymentGroup, {
            services: result.successes
          }));
        });
      });
    };

    const handleDeploymentGroups = (err, deploymentGroups) => {
      if (err) {
        return cb(err);
      }

      VAsync.forEachParallel({
        inputs: deploymentGroups,
        func: getServices
      }, (err, result) => {
        cb(err, result.successes);
      });
    };

    const getDeploymentGroups = (err, portal) => {
      if (err) {
        return cb(err);
      }

      portal.deploymentGroups({}, handleDeploymentGroups);
    };

    this._data.getPortal({}, getDeploymentGroups);
  }

  _getNetworks (networkIds = [], cb) {
    VAsync.forEachParallel({
      inputs: networkIds,
      func: (id, next) => {
        this._triton.getNetwork(id, next);
      }
    }, (err, results) => {
      cb(err, ForceArray((results || {}).successes));
    });
  }

  _getPublicIps (machine, cb) {
    this._getNetworks(machine.networks, (err, networks) => {
      if (err) {
        return cb(err);
      }

      const privateNetworkSubnets = networks
        .filter((network) => {
          return !network['public'];
        })
        .map((network) => {
          return network.subnet;
        })
        .filter(Boolean);

      const cidr = new CIDRMatcher(privateNetworkSubnets);

      const nonPrivateIps = machine.ips.filter((ip) => {
        return !cidr.contains(ip);
      });

      cb(null, nonPrivateIps);
    });
  }

  _fetchInstanceStatus (instance, cb) {
    const { machineId } = instance;

    const handleStatuses = (err, results) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      const statuses = ForceArray((results || {}).successes);

      const status = statuses.filter(Boolean).shift();

      if (!status) {
        return cb(null, instance);
      }

      const services = ForceArray(status.Services).filter(({ Name }) => {
        return Name !== 'containerpilot';
      });

      cb(null, Object.assign({}, instance, {
        cp: Object.assign({}, status, {
          Services: services
        })
      }));
    };

    const fetchStatus = (ip, next) => {
      Wreck.get(`http://${ip}:9090/status`, {
        timeout: 1000 // 1s
      }, (err, res, payload) => {
        if (err) {
          this.emit('error', err);
          return next();
        }

        if (Buffer.isBuffer(payload)) {
          payload = payload.toString();
        }

        try {
          const status = JSON.parse(payload);
          next(null, status);
        } catch (err) {
          this.emit('error', err);
          next();
        }
      });
    };

    const handlePublicIps = (err, ips) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      VAsync.forEachParallel({
        inputs: ips,
        func: fetchStatus
      }, handleStatuses);
    };

    this._triton.getMachine(machineId, (err, machine) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      this._getPublicIps(machine, handlePublicIps);
    });
  }

  _fetchServiceStatus (service, cb) {
    VAsync.forEachParallel({
      inputs: service.instances,
      func: (instance, next) => {
        this._fetchInstanceStatus(instance, next);
      }
    }, (err, results) => {
      if (err) {
        this.emit('error', err);
      }

      cb(null, Object.assign({}, service, {
        instances: ForceArray((results || {}).successes)
      }));
    });
  }

  _fetchDeploymentGroupStatus (dg, cb) {
    if (!dg || !dg.services || !dg.services.length) {
      return cb();
    }

    VAsync.forEachParallel({
      inputs: dg.services,
      func: (service, next) => {
        this._fetchServiceStatus(service, next);
      }
    }, (err, results) => {
      if (err) {
        this.emit('error', err);
      }

      cb(null, Object.assign({}, dg, {
        services: ForceArray((results || {}).successes)
      }));
    });
  }

  _saveInstance ({ id, healthy, watches, jobs }, cb) {
    if (!id) {
      return cb();
    }

    this._data.updateInstance({
      id,
      healthy,
      watches,
      jobs
    }, cb);
  }

  _saveService ({ id, instances, connections, branches }, cb) {
    if (!id) {
      return cb();
    }

    VAsync.forEachParallel({
      inputs: instances,
      func: (instance, next) => {
        this._saveInstance(instance, next);
      }
    }, (err) => {
      if (err) {
        return cb(err);
      }

      this._data.updateService({
        id,
        connections,
        branches
      }, cb);
    });
  }

  _saveDeploymentGroup (dg, cb) {
    VAsync.forEachParallel({
      inputs: dg.services,
      func: (service, next) => {
        this._saveService(service, next);
      }
    }, cb);
  }

  static _resolveServiceConnections ({ services, service }) {
    const watches = Uniq(
      Flatten(
        ForceArray(service.instances).map(({ watches }) => {
          return watches;
        })
      )
    );

    return watches
      .map((jobName) => {
        return services.reduce((serviceId, service) => {
          if (serviceId) {
            return serviceId;
          }

          const thisServiceJobs = Uniq(
            Flatten(
              ForceArray(service.instances).map(({ jobs }) => {
                return jobs;
              })
            )
          );

          if (thisServiceJobs.indexOf(jobName) >= 0) {
            return service.id;
          }

          return serviceId;
        }, null);
      })
      .filter(Boolean)
      .filter((serviceId) => {
        return service.id !== serviceId;
      });
  }

  static _resolveInstanceHealth ({ name }, instance) {
    if (!instance) {
      return 'UNAVAILABLE';
    }

    if (!instance.cp) {
      return 'UNAVAILABLE';
    }

    const jobNames = Get(instance, 'cp.Services');

    const serviceJobs = jobNames.filter(({ Name }) => {
      return Name === name;
    });

    if (serviceJobs.length) {
      return serviceJobs.shift().Status.toUpperCase();
    }

    const almostJobNameRegexp = new RegExp(`${name}-.*`);
    const almostServiceJobs = jobNames.filter(({ Name }) => {
      return almostJobNameRegexp.test(Name);
    });

    if (almostServiceJobs.length) {
      return almostServiceJobs.shift().Status.toUpperCase();
    }

    return 'UNKNOWN';
  }

  static _resolveServiceBranches ({ name, slug, instances }) {
    if (instances.length <= 1) {
      return [];
    }

    const deviantJobNames = Uniq(Flatten(instances.map(({ jobs }) => {
      return Flatten(jobs.filter((jobName) => {
        return new RegExp(`${name}-.*`).test(jobName);
      }));
    })));

    if (!deviantJobNames || !deviantJobNames.length) {
      return [];
    }

    const defaultBranch = instances.reduce((service, { id, jobs }) => {
      if (jobs.indexOf(name) >= 0) {
        return Object.assign(service, {
          instances: service.instances.concat(id)
        });
      }

      return service;
    }, {
      name: name,
      slug: slug,
      instances: []
    });

    defaultBranch.id = Hasha(JSON.stringify(defaultBranch));

    const branches = instances.reduce((branches, { id, jobs }) => {
      if (defaultBranch.instances.indexOf(id) >= 0) {
        return branches;
      }

      const branchName = jobs
        .filter((jobName) => {
          return deviantJobNames.indexOf(jobName) >= 0;
        })
        .shift();

      if (!branches[branchName]) {
        branches[branchName] = {
          name: branchName,
          slug: ParamCase(branchName),
          instances: []
        };

        branches[branchName].id = Hasha(JSON.stringify(branches[branchName]));
      }

      branches[branchName].instances.push(id);

      return branches;
    }, {});

    return Object.values(branches).concat(defaultBranch);
  }

  static _resolveDeploymentGroups (dgs) {
    return dgs
      .filter(Boolean)
      .map((dg) => {
        return Object.assign({}, dg, {
          services: ForceArray(dg.services).map((service) => {
            return Object.assign({}, service, {
              instances: ForceArray(service.instances).map((instance) => {
                const watches = Get(instance, 'cp.Watches', []);
                const jobs = Get(instance, 'cp.Services', []).map(({ Name }) => {
                  return Name;
                });

                return Object.assign({}, instance, {
                  healthy: ContainerPilotWatcher._resolveInstanceHealth(service, instance),
                  jobs,
                  watches
                });
              })
            });
          })
        });
      })
      .map((dg) => {
        return Object.assign({}, dg, {
          services: ForceArray(dg.services).map((service) => {
            return Object.assign({}, service, {
              branches: ContainerPilotWatcher._resolveServiceBranches(service),
              connections: ContainerPilotWatcher._resolveServiceConnections({
                services: dg.services,
                service
              })
            });
          })
        });
      });
  }

  check (cb) {
    if (!this._triton) {
      return cb();
    }

    const handleStatuses = (err, results) => {
      if (err) {
        this.emit('error', err);
      }

      const dgs = ContainerPilotWatcher._resolveDeploymentGroups(
        ForceArray((results || {}).successes)
      );

      VAsync.forEachParallel({
        inputs: dgs,
        func: (dg, next) => {
          Queue(dg.id, () => {
            return new Promise((resolve) => {
              this._saveDeploymentGroup(dg, (err) => {
                resolve();
                next(err);
              });
            });
          });
        }
      }, (err) => {
        if (err) {
          this.emit('error', err);
        }

        cb(null, dgs);
      });
    };

    const fetchStatuses = (err, dgs) => {
      if (err) {
        this.emit('error', err);
        return cb();
      }

      VAsync.forEachParallel({
        inputs: dgs,
        func: (dg, next) => {
          this._fetchDeploymentGroupStatus(dg, next);
        }
      }, handleStatuses);
    };

    this._getDeploymentGroups(fetchStatuses);
  }
};

<|endoftext|>


<commit_before>// Copyright 2015-2020, University of Colorado Boulder

/**
 * View for demonstrating dialogs.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import GeneralKeyboardHelpSection from '../../../scenery-phet/js/keyboard/help/GeneralKeyboardHelpSection.js';
import VBox from '../../../scenery/js/nodes/VBox.js';
import Panel from '../../../sun/js/Panel.js';
import Tandem from '../../../tandem/js/Tandem.js';
import joist from '../joist.js';
import KeyboardHelpButton from '../KeyboardHelpButton.js';
import ScreenView from '../ScreenView.js';

class DialogsScreenView extends ScreenView {
  constructor() {

    super();

    const keyboardHelpDialogContent = new GeneralKeyboardHelpSection();

    const keyboardHelpButton = new KeyboardHelpButton(
      keyboardHelpDialogContent,
      phet.joist.sim.lookAndFeel.navigationBarFillProperty,
      Tandem.GENERAL.createTandem( 'keyboardHelpButton' )
    );
    keyboardHelpButton.setScaleMagnitude( 2 );

    // Since KeyboardHelpButton adapts its color to the navigation bar,
    // put the button in a panel that's the same color as the navigation bar
    const keyboardHelpPanel = new Panel( keyboardHelpButton, {
      fill: phet.joist.sim.lookAndFeel.navigationBarFillProperty.value
    } );

    this.addChild( new VBox( {
      children: [
        keyboardHelpPanel
      ],
      spacing: 20,
      center: this.layoutBounds.center
    } ) );
  }
}

joist.register( 'DialogsScreenView', DialogsScreenView );
export default DialogsScreenView;<commit_msg>dynamically change panel fill to match navigation bar, https://github.com/phetsims/joist/issues/663<commit_after>// Copyright 2015-2020, University of Colorado Boulder

/**
 * View for demonstrating dialogs.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import GeneralKeyboardHelpSection from '../../../scenery-phet/js/keyboard/help/GeneralKeyboardHelpSection.js';
import VBox from '../../../scenery/js/nodes/VBox.js';
import Panel from '../../../sun/js/Panel.js';
import Tandem from '../../../tandem/js/Tandem.js';
import joist from '../joist.js';
import KeyboardHelpButton from '../KeyboardHelpButton.js';
import ScreenView from '../ScreenView.js';

class DialogsScreenView extends ScreenView {
  constructor() {

    super();

    const keyboardHelpDialogContent = new GeneralKeyboardHelpSection();

    const keyboardHelpButton = new KeyboardHelpButton(
      keyboardHelpDialogContent,
      phet.joist.sim.lookAndFeel.navigationBarFillProperty,
      Tandem.GENERAL.createTandem( 'keyboardHelpButton' )
    );
    keyboardHelpButton.setScaleMagnitude( 2 );

    // Since KeyboardHelpButton adapts its color to the navigation bar,
    // put the button in a panel that's the same color as the navigation bar
    const keyboardHelpPanel = new Panel( keyboardHelpButton, {
      fill: phet.joist.sim.lookAndFeel.navigationBarFillProperty.value
    } );
    phet.joist.sim.lookAndFeel.navigationBarFillProperty.link( navigationBarFill => {
      keyboardHelpPanel.setFill( navigationBarFill );
    } );

    this.addChild( new VBox( {
      children: [
        keyboardHelpPanel
      ],
      spacing: 20,
      center: this.layoutBounds.center
    } ) );
  }
}

joist.register( 'DialogsScreenView', DialogsScreenView );
export default DialogsScreenView;<|endoftext|>


<commit_before>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { isForwardRef } from 'react-is';
import debounce from 'lodash.debounce';
import classNames from 'classnames';
import { Information16 as Information } from '@carbon/icons-react';
import { settings } from 'carbon-components';
import FloatingMenu, {
  DIRECTION_LEFT,
  DIRECTION_TOP,
  DIRECTION_RIGHT,
  DIRECTION_BOTTOM,
} from '../../internal/FloatingMenu';
import ClickListener from '../../internal/ClickListener';
import mergeRefs from '../../tools/mergeRefs';
import { keys, matches as keyDownMatch } from '../../internal/keyboard';
import isRequiredOneOf from '../../prop-types/isRequiredOneOf';
import requiredIfValueExists from '../../prop-types/requiredIfValueExists';
import { useControlledStateWithValue } from '../../internal/FeatureFlags';

const { prefix } = settings;

/**
 * @param {Element} menuBody The menu body with the menu arrow.
 * @param {string} menuDirection Where the floating menu menu should be placed relative to the trigger button.
 * @returns {FloatingMenu~offset} The adjustment of the floating menu position, upon the position of the menu arrow.
 * @private
 */
const getMenuOffset = (menuBody, menuDirection) => {
  const arrowStyle = menuBody.ownerDocument.defaultView.getComputedStyle(
    menuBody,
    ':before'
  );
  const arrowPositionProp = {
    [DIRECTION_LEFT]: 'right',
    [DIRECTION_TOP]: 'bottom',
    [DIRECTION_RIGHT]: 'left',
    [DIRECTION_BOTTOM]: 'top',
  }[menuDirection];
  const menuPositionAdjustmentProp = {
    [DIRECTION_LEFT]: 'left',
    [DIRECTION_TOP]: 'top',
    [DIRECTION_RIGHT]: 'left',
    [DIRECTION_BOTTOM]: 'top',
  }[menuDirection];
  const values = [arrowPositionProp, 'border-bottom-width'].reduce(
    (o, name) => ({
      ...o,
      [name]: Number(
        (/^([\d-]+)px$/.exec(arrowStyle.getPropertyValue(name)) || [])[1]
      ),
    }),
    {}
  );
  values[arrowPositionProp] = values[arrowPositionProp] || -6; // IE, etc.
  if (Object.keys(values).every((name) => !isNaN(values[name]))) {
    const {
      [arrowPositionProp]: arrowPosition,
      'border-bottom-width': borderBottomWidth,
    } = values;
    return {
      left: 0,
      top: 0,
      [menuPositionAdjustmentProp]:
        Math.sqrt(Math.pow(borderBottomWidth, 2) * 2) - arrowPosition,
    };
  }
};

class Tooltip extends Component {
  constructor(props) {
    super(props);
    this.isControlled = props.open !== undefined;
    if (useControlledStateWithValue && this.isControlled) {
      // Skips the logic of setting initial state if this component is controlled
      return;
    }
    const open = useControlledStateWithValue ? props.defaultOpen : props.open;
    this.state = { open };
  }

  static propTypes = {
    /**
     * Specify the alignment (to the trigger button) of the tooltip.
     * Can be one of: start, center, or end.
     */
    align: PropTypes.oneOf(['start', 'center', 'end']),

    /**
     * Contents to put into the tooltip.
     */
    children: PropTypes.node,

    /**
     * The CSS class names of the tooltip.
     */
    className: PropTypes.string,

    /**
     * Optional starting value for uncontrolled state
     */
    defaultOpen: PropTypes.bool,

    /**
     * Where to put the tooltip, relative to the trigger UI.
     */
    direction: PropTypes.oneOf(['bottom', 'top', 'left', 'right']),

    /**
     * Enable or disable focus trap behavior
     */
    focusTrap: PropTypes.bool,

    /**
     * The name of the default tooltip icon.
     */
    iconName: PropTypes.string,

    /**
     * The adjustment of the tooltip position.
     */
    menuOffset: PropTypes.oneOfType([
      PropTypes.shape({
        top: PropTypes.number,
        left: PropTypes.number,
      }),
      PropTypes.func,
    ]),

    /**
     * * the signature of the event handler will be:
     * * `onChange(event, { open })` where:
     *   * `event` is the (React) raw event
     *   * `open` is the new value
     */
    onChange: !useControlledStateWithValue
      ? PropTypes.func
      : requiredIfValueExists(PropTypes.func),

    /**
     * Open/closed state.
     */
    open: PropTypes.bool,

    /**
     * The callback function to optionally render the icon element.
     * It should be a component with React.forwardRef().
     */
    renderIcon: function (props, propName, componentName) {
      if (props[propName] == undefined) {
        return;
      }
      const RefForwardingComponent = props[propName];
      if (!isForwardRef(<RefForwardingComponent />))
        return new Error(`Invalid value of prop '${propName}' supplied to '${componentName}',
                          it should be created/wrapped with React.forwardRef() to have a ref and access the proper
                          DOM node of the element to calculate its position in the viewport.`);
    },

    /**
     * Specify a CSS selector that matches the DOM element that should
     * be focused when the Tooltip opens
     */
    selectorPrimaryFocus: PropTypes.string,

    /**
     * `true` to show the default tooltip icon.
     */
    showIcon: PropTypes.bool,

    /**
     * Optional prop to specify the tabIndex of the Tooltip
     */
    tabIndex: PropTypes.number,

    /**
     * The ID of the tooltip body content.
     */
    tooltipBodyId: PropTypes.string,

    /**
     * The ID of the tooltip content.
     */
    tooltipId: PropTypes.string,

    /**
     * The CSS class names of the trigger UI.
     */
    triggerClassName: PropTypes.string,

    /**
     * The ID of the trigger button.
     */
    triggerId: PropTypes.string,

    ...isRequiredOneOf({
      /**
       * The content to put into the trigger UI, except the (default) tooltip icon.
       */
      triggerText: PropTypes.node,
      /**
       * The description of the default tooltip icon, to be put in its SVG 'aria-label' and 'alt' .
       */
      iconDescription: PropTypes.string,
    }),
  };

  static defaultProps = {
    align: 'center',
    direction: DIRECTION_BOTTOM,
    focusTrap: true,
    renderIcon: Information,
    showIcon: true,
    triggerText: null,
    menuOffset: getMenuOffset,
    selectorPrimaryFocus: '[data-tooltip-primary-focus]',
  };

  /**
   * The element of the tooltip body.
   * @type {Element}
   * @private
   */
  _tooltipEl = null;

  /**
   * The element ref of the tooltip's trigger button.
   * @type {React.RefObject<Element>}
   * @private
   */
  _triggerRef = React.createRef();

  /**
   * Unique tooltip ID that is user-provided or auto-generated
   * Referenced in aria-labelledby attribute
   * @type {string}
   * @private
   */
  _tooltipId =
    this.props.tooltipId ||
    `__carbon-tooltip_${Math.random().toString(36).substr(2)}`;

  /**
   * Internal flag for tracking whether or not focusing on the tooltip trigger
   * should automatically display the tooltip body
   */
  _tooltipDismissed = false;

  componentDidMount() {
    if (!this._debouncedHandleFocus) {
      this._debouncedHandleFocus = debounce(this._handleFocus, 200);
    }

    document.addEventListener('keydown', this.handleEscKeyPress, false);
  }

  componentWillUnmount() {
    if (this._debouncedHandleFocus) {
      this._debouncedHandleFocus.cancel();
      this._debouncedHandleFocus = null;
    }

    document.removeEventListener('keydown', this.handleEscKeyPress, false);
  }

  static getDerivedStateFromProps({ open }, state) {
    /**
     * so that tooltip can be controlled programmatically through this `open` prop
     */
    const { prevOpen } = state;
    return prevOpen === open
      ? null
      : {
          open,
          prevOpen: open,
        };
  }

  _handleUserInputOpenClose = (event, { open }) => {
    if (this.isControlled) {
      return;
    }
    // capture tooltip body element before it is removed from the DOM
    const tooltipBody = this._tooltipEl;
    this.setState({ open }, () => {
      if (this.props.onChange) {
        this.props.onChange(event, { open });
      }
      if (!open && tooltipBody && tooltipBody.id === this._tooltipId) {
        this._tooltipDismissed = true;
        this._triggerRef?.current.focus();
      }
    });
  };

  /**
   * Handles `focus`/`blur` event.
   * @param {string} state `over` to show the tooltip, `out` to hide the tooltip.
   * @param {Element} [evt] For handing `mouseout` event, indicates where the mouse pointer is gone.
   */
  _handleFocus = (state, evt) => {
    const { relatedTarget } = evt;
    if (state === 'over') {
      if (!this._tooltipDismissed) {
        this._handleUserInputOpenClose(evt, { open: true });
      }
      this._tooltipDismissed = false;
    } else {
      // Note: SVGElement in IE11 does not have `.contains()`
      const { current: triggerEl } = this._triggerRef;
      const shouldPreventClose =
        relatedTarget &&
        ((triggerEl && triggerEl?.contains(relatedTarget)) ||
          (this._tooltipEl && this._tooltipEl.contains(relatedTarget)));
      if (!shouldPreventClose) {
        this._handleUserInputOpenClose(evt, { open: false });
      }
    }
  };

  /**
   * The debounced version of the `focus`/`blur` event handler.
   * @type {Function}
   * @private
   */
  _debouncedHandleFocus = null;

  /**
   * @returns {Element} The DOM element where the floating menu is placed in.
   */
  _getTarget = () => {
    const { current: triggerEl } = this._triggerRef;
    return (
      (triggerEl && triggerEl.closest('[data-floating-menu-container]')) ||
      document.body
    );
  };

  handleMouse = (evt) => {
    evt.persist();
    const state = {
      focus: 'over',
      blur: 'out',
      click: 'click',
    }[evt.type];
    const hadContextMenu = this._hasContextMenu;
    this._hasContextMenu = evt.type === 'contextmenu';
    if (state === 'click') {
      evt.stopPropagation();
      evt.preventDefault();
      const shouldOpen = this.isControlled
        ? !this.props.open
        : !this.state.open;
      this._handleUserInputOpenClose(evt, { open: shouldOpen });
    } else if (
      state &&
      (state !== 'out' || !hadContextMenu) &&
      this._debouncedHandleFocus
    ) {
      this._debouncedHandleFocus(state, evt);
    }
  };

  handleClickOutside = (evt) => {
    const shouldPreventClose =
      evt &&
      evt.target &&
      this._tooltipEl &&
      this._tooltipEl.contains(evt.target);
    if (!shouldPreventClose && this.state.open) {
      this._handleUserInputOpenClose(evt, { open: false });
    }
  };

  handleKeyPress = (event) => {
    if (keyDownMatch(event, [keys.Escape])) {
      event.stopPropagation();
      this._handleUserInputOpenClose(event, { open: false });
    }

    if (keyDownMatch(event, [keys.Enter, keys.Space])) {
      event.stopPropagation();
      event.preventDefault();
      const shouldOpen = this.isControlled
        ? !this.props.open
        : !this.state.open;
      this._handleUserInputOpenClose(event, { open: shouldOpen });
    }
  };

  handleEscKeyPress = (event) => {
    const { open } = this.isControlled ? this.props : this.state;
    if (open && keyDownMatch(event, [keys.Escape])) {
      return this._handleUserInputOpenClose(event, { open: false });
    }
  };

  render() {
    const {
      triggerId = (this.triggerId =
        this.triggerId ||
        `__carbon-tooltip-trigger_${Math.random().toString(36).substr(2)}`),
      tooltipBodyId,
      children,
      className,
      triggerClassName,
      direction,
      align,
      focusTrap,
      triggerText,
      showIcon,
      iconName,
      iconDescription,
      renderIcon: IconCustomElement,
      menuOffset,
      tabIndex = 0,
      innerRef: ref,
      selectorPrimaryFocus, // eslint-disable-line
      ...other
    } = this.props;

    const { open } = this.isControlled ? this.props : this.state;

    const tooltipClasses = classNames(
      `${prefix}--tooltip`,
      {
        [`${prefix}--tooltip--shown`]: open,
        [`${prefix}--tooltip--${direction}`]: direction,
        [`${prefix}--tooltip--align-${align}`]: align,
      },
      className
    );

    const triggerClasses = classNames(
      `${prefix}--tooltip__label`,
      triggerClassName
    );

    const refProp = mergeRefs(this._triggerRef, ref);
    const iconProperties = { name: iconName, role: null, description: null };

    const properties = {
      role: 'button',
      tabIndex: tabIndex,
      onClick: this.handleMouse,
      onKeyDown: this.handleKeyPress,
      onMouseOver: this.handleMouse,
      onMouseOut: this.handleMouse,
      onFocus: this.handleMouse,
      onBlur: this.handleMouse,
      'aria-controls': !open ? undefined : this._tooltipId,
      'aria-expanded': open,
      'aria-describedby': open ? this._tooltipId : null,
      // if the user provides property `triggerText`,
      // then the button should use aria-labelledby to point to its id,
      // if the user doesn't provide property `triggerText`,
      // then an aria-label will be provided via the `iconDescription` property.
      ...(triggerText
        ? {
            'aria-labelledby': triggerId,
          }
        : {
            'aria-label': iconDescription,
          }),
    };

    return (
      <>
        <ClickListener onClickOutside={this.handleClickOutside}>
          {showIcon ? (

              {triggerText}
              <div
                className={`${prefix}--tooltip__trigger`}
                {...properties}
                ref={refProp}>
                <IconCustomElement {...iconProperties} />

          ) : (

              {triggerText}

          )}
        </ClickListener>
        {open && (
          <FloatingMenu
            focusTrap={focusTrap}
            selectorPrimaryFocus={this.props.selectorPrimaryFocus}
            target={this._getTarget}
            triggerRef={this._triggerRef}
            menuDirection={direction}
            menuOffset={menuOffset}
            menuRef={(node) => {
              this._tooltipEl = node;
            }}>

              <span className={`${prefix}--tooltip__caret`} />
              <div
                className={`${prefix}--tooltip__content`}
                tabIndex="-1"
                role="dialog"
                aria-describedby={tooltipBodyId}
                aria-labelledby={triggerId}>
                {children}

          </FloatingMenu>
        )}
      </>
    );
  }
}

export { Tooltip };
export default (() => {
  const forwardRef = (props, ref) => <Tooltip {...props} innerRef={ref} />;
  forwardRef.displayName = 'Tooltip';
  return React.forwardRef(forwardRef);
})();
<commit_msg>fix(Tooltip): prevent focus hijacking on tooltip dismissal (#7319)<commit_after>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { isForwardRef } from 'react-is';
import debounce from 'lodash.debounce';
import classNames from 'classnames';
import { Information16 as Information } from '@carbon/icons-react';
import { settings } from 'carbon-components';
import FloatingMenu, {
  DIRECTION_LEFT,
  DIRECTION_TOP,
  DIRECTION_RIGHT,
  DIRECTION_BOTTOM,
} from '../../internal/FloatingMenu';
import ClickListener from '../../internal/ClickListener';
import mergeRefs from '../../tools/mergeRefs';
import { keys, matches as keyDownMatch } from '../../internal/keyboard';
import {
  selectorFocusable,
  selectorTabbable,
} from '../../internal/keyboard/navigation';
import isRequiredOneOf from '../../prop-types/isRequiredOneOf';
import requiredIfValueExists from '../../prop-types/requiredIfValueExists';
import { useControlledStateWithValue } from '../../internal/FeatureFlags';

const { prefix } = settings;

/**
 * @param {Element} menuBody The menu body with the menu arrow.
 * @param {string} menuDirection Where the floating menu menu should be placed relative to the trigger button.
 * @returns {FloatingMenu~offset} The adjustment of the floating menu position, upon the position of the menu arrow.
 * @private
 */
const getMenuOffset = (menuBody, menuDirection) => {
  const arrowStyle = menuBody.ownerDocument.defaultView.getComputedStyle(
    menuBody,
    ':before'
  );
  const arrowPositionProp = {
    [DIRECTION_LEFT]: 'right',
    [DIRECTION_TOP]: 'bottom',
    [DIRECTION_RIGHT]: 'left',
    [DIRECTION_BOTTOM]: 'top',
  }[menuDirection];
  const menuPositionAdjustmentProp = {
    [DIRECTION_LEFT]: 'left',
    [DIRECTION_TOP]: 'top',
    [DIRECTION_RIGHT]: 'left',
    [DIRECTION_BOTTOM]: 'top',
  }[menuDirection];
  const values = [arrowPositionProp, 'border-bottom-width'].reduce(
    (o, name) => ({
      ...o,
      [name]: Number(
        (/^([\d-]+)px$/.exec(arrowStyle.getPropertyValue(name)) || [])[1]
      ),
    }),
    {}
  );
  values[arrowPositionProp] = values[arrowPositionProp] || -6; // IE, etc.
  if (Object.keys(values).every((name) => !isNaN(values[name]))) {
    const {
      [arrowPositionProp]: arrowPosition,
      'border-bottom-width': borderBottomWidth,
    } = values;
    return {
      left: 0,
      top: 0,
      [menuPositionAdjustmentProp]:
        Math.sqrt(Math.pow(borderBottomWidth, 2) * 2) - arrowPosition,
    };
  }
};

class Tooltip extends Component {
  constructor(props) {
    super(props);
    this.isControlled = props.open !== undefined;
    if (useControlledStateWithValue && this.isControlled) {
      // Skips the logic of setting initial state if this component is controlled
      return;
    }
    const open = useControlledStateWithValue ? props.defaultOpen : props.open;
    this.state = { open };
  }

  static propTypes = {
    /**
     * Specify the alignment (to the trigger button) of the tooltip.
     * Can be one of: start, center, or end.
     */
    align: PropTypes.oneOf(['start', 'center', 'end']),

    /**
     * Contents to put into the tooltip.
     */
    children: PropTypes.node,

    /**
     * The CSS class names of the tooltip.
     */
    className: PropTypes.string,

    /**
     * Optional starting value for uncontrolled state
     */
    defaultOpen: PropTypes.bool,

    /**
     * Where to put the tooltip, relative to the trigger UI.
     */
    direction: PropTypes.oneOf(['bottom', 'top', 'left', 'right']),

    /**
     * Enable or disable focus trap behavior
     */
    focusTrap: PropTypes.bool,

    /**
     * The name of the default tooltip icon.
     */
    iconName: PropTypes.string,

    /**
     * The adjustment of the tooltip position.
     */
    menuOffset: PropTypes.oneOfType([
      PropTypes.shape({
        top: PropTypes.number,
        left: PropTypes.number,
      }),
      PropTypes.func,
    ]),

    /**
     * * the signature of the event handler will be:
     * * `onChange(event, { open })` where:
     *   * `event` is the (React) raw event
     *   * `open` is the new value
     */
    onChange: !useControlledStateWithValue
      ? PropTypes.func
      : requiredIfValueExists(PropTypes.func),

    /**
     * Open/closed state.
     */
    open: PropTypes.bool,

    /**
     * The callback function to optionally render the icon element.
     * It should be a component with React.forwardRef().
     */
    renderIcon: function (props, propName, componentName) {
      if (props[propName] == undefined) {
        return;
      }
      const RefForwardingComponent = props[propName];
      if (!isForwardRef(<RefForwardingComponent />))
        return new Error(`Invalid value of prop '${propName}' supplied to '${componentName}',
                          it should be created/wrapped with React.forwardRef() to have a ref and access the proper
                          DOM node of the element to calculate its position in the viewport.`);
    },

    /**
     * Specify a CSS selector that matches the DOM element that should
     * be focused when the Tooltip opens
     */
    selectorPrimaryFocus: PropTypes.string,

    /**
     * `true` to show the default tooltip icon.
     */
    showIcon: PropTypes.bool,

    /**
     * Optional prop to specify the tabIndex of the Tooltip
     */
    tabIndex: PropTypes.number,

    /**
     * The ID of the tooltip body content.
     */
    tooltipBodyId: PropTypes.string,

    /**
     * The ID of the tooltip content.
     */
    tooltipId: PropTypes.string,

    /**
     * The CSS class names of the trigger UI.
     */
    triggerClassName: PropTypes.string,

    /**
     * The ID of the trigger button.
     */
    triggerId: PropTypes.string,

    ...isRequiredOneOf({
      /**
       * The content to put into the trigger UI, except the (default) tooltip icon.
       */
      triggerText: PropTypes.node,
      /**
       * The description of the default tooltip icon, to be put in its SVG 'aria-label' and 'alt' .
       */
      iconDescription: PropTypes.string,
    }),
  };

  static defaultProps = {
    align: 'center',
    direction: DIRECTION_BOTTOM,
    focusTrap: true,
    renderIcon: Information,
    showIcon: true,
    triggerText: null,
    menuOffset: getMenuOffset,
    selectorPrimaryFocus: '[data-tooltip-primary-focus]',
  };

  /**
   * The element of the tooltip body.
   * @type {Element}
   * @private
   */
  _tooltipEl = null;

  /**
   * The element ref of the tooltip's trigger button.
   * @type {React.RefObject<Element>}
   * @private
   */
  _triggerRef = React.createRef();

  /**
   * Unique tooltip ID that is user-provided or auto-generated
   * Referenced in aria-labelledby attribute
   * @type {string}
   * @private
   */
  _tooltipId =
    this.props.id ||
    this.props.tooltipId ||
    `__carbon-tooltip_${Math.random().toString(36).substr(2)}`;

  /**
   * Internal flag for tracking whether or not focusing on the tooltip trigger
   * should automatically display the tooltip body
   */
  _tooltipDismissed = false;

  componentDidMount() {
    if (!this._debouncedHandleFocus) {
      this._debouncedHandleFocus = debounce(this._handleFocus, 200);
    }

    document.addEventListener('keydown', this.handleEscKeyPress, false);
  }

  componentWillUnmount() {
    if (this._debouncedHandleFocus) {
      this._debouncedHandleFocus.cancel();
      this._debouncedHandleFocus = null;
    }

    document.removeEventListener('keydown', this.handleEscKeyPress, false);
  }

  static getDerivedStateFromProps({ open }, state) {
    /**
     * so that tooltip can be controlled programmatically through this `open` prop
     */
    const { prevOpen } = state;
    return prevOpen === open
      ? null
      : {
          open,
          prevOpen: open,
        };
  }

  _handleUserInputOpenClose = (event, { open }) => {
    if (this.isControlled) {
      return;
    }
    // capture tooltip body element before it is removed from the DOM
    const tooltipBody = this._tooltipEl;
    this.setState({ open }, () => {
      if (this.props.onChange) {
        this.props.onChange(event, { open });
      }
      if (!open && tooltipBody && tooltipBody.id === this._tooltipId) {
        this._tooltipDismissed = true;
        const primaryFocusNode = tooltipBody.querySelector(
          this.props.selectorPrimaryFocus || null
        );
        const tabbableNode = tooltipBody.querySelector(selectorTabbable);
        const focusableNode = tooltipBody.querySelector(selectorFocusable);
        const focusTarget =
          primaryFocusNode || // User defined focusable node
          tabbableNode || // First sequentially focusable node
          focusableNode || // First programmatic focusable node
          tooltipBody;
        if (focusTarget !== tooltipBody) {
          this._triggerRef?.current.focus();
        }
      }
    });
  };

  /**
   * Handles `focus`/`blur` event.
   * @param {string} state `over` to show the tooltip, `out` to hide the tooltip.
   * @param {Element} [evt] For handing `mouseout` event, indicates where the mouse pointer is gone.
   */
  _handleFocus = (state, evt) => {
    const { currentTarget, relatedTarget } = evt;
    if (currentTarget !== relatedTarget) {
      this._tooltipDismissed = false;
    }
    if (state === 'over') {
      if (!this._tooltipDismissed) {
        this._handleUserInputOpenClose(evt, { open: true });
      }
      this._tooltipDismissed = false;
    } else {
      // Note: SVGElement in IE11 does not have `.contains()`
      const { current: triggerEl } = this._triggerRef;
      const shouldPreventClose =
        relatedTarget &&
        ((triggerEl && triggerEl?.contains(relatedTarget)) ||
          (this._tooltipEl && this._tooltipEl.contains(relatedTarget)));
      if (!shouldPreventClose) {
        this._handleUserInputOpenClose(evt, { open: false });
      }
    }
  };

  /**
   * The debounced version of the `focus`/`blur` event handler.
   * @type {Function}
   * @private
   */
  _debouncedHandleFocus = null;

  /**
   * @returns {Element} The DOM element where the floating menu is placed in.
   */
  _getTarget = () => {
    const { current: triggerEl } = this._triggerRef;
    return (
      (triggerEl && triggerEl.closest('[data-floating-menu-container]')) ||
      document.body
    );
  };

  handleMouse = (evt) => {
    evt.persist();
    const state = {
      focus: 'over',
      blur: 'out',
      click: 'click',
    }[evt.type];
    const hadContextMenu = this._hasContextMenu;
    this._hasContextMenu = evt.type === 'contextmenu';
    if (state === 'click') {
      evt.stopPropagation();
      evt.preventDefault();
      const shouldOpen = this.isControlled
        ? !this.props.open
        : !this.state.open;
      this._handleUserInputOpenClose(evt, { open: shouldOpen });
    } else if (
      state &&
      (state !== 'out' || !hadContextMenu) &&
      this._debouncedHandleFocus
    ) {
      this._debouncedHandleFocus(state, evt);
    }
  };

  handleClickOutside = (evt) => {
    const shouldPreventClose =
      evt &&
      evt.target &&
      this._tooltipEl &&
      this._tooltipEl.contains(evt.target);
    if (!shouldPreventClose && this.state.open) {
      this._handleUserInputOpenClose(evt, { open: false });
    }
  };

  handleKeyPress = (event) => {
    if (keyDownMatch(event, [keys.Escape])) {
      event.stopPropagation();
      this._handleUserInputOpenClose(event, { open: false });
    }

    if (keyDownMatch(event, [keys.Enter, keys.Space])) {
      event.stopPropagation();
      event.preventDefault();
      const shouldOpen = this.isControlled
        ? !this.props.open
        : !this.state.open;
      this._handleUserInputOpenClose(event, { open: shouldOpen });
    }
  };

  handleEscKeyPress = (event) => {
    const { open } = this.isControlled ? this.props : this.state;
    if (open && keyDownMatch(event, [keys.Escape])) {
      return this._handleUserInputOpenClose(event, { open: false });
    }
  };

  render() {
    const {
      triggerId = (this.triggerId =
        this.triggerId ||
        `__carbon-tooltip-trigger_${Math.random().toString(36).substr(2)}`),
      tooltipBodyId,
      children,
      className,
      triggerClassName,
      direction,
      align,
      focusTrap,
      triggerText,
      showIcon,
      iconName,
      iconDescription,
      renderIcon: IconCustomElement,
      menuOffset,
      tabIndex = 0,
      innerRef: ref,
      selectorPrimaryFocus, // eslint-disable-line
      ...other
    } = this.props;

    const { open } = this.isControlled ? this.props : this.state;

    const tooltipClasses = classNames(
      `${prefix}--tooltip`,
      {
        [`${prefix}--tooltip--shown`]: open,
        [`${prefix}--tooltip--${direction}`]: direction,
        [`${prefix}--tooltip--align-${align}`]: align,
      },
      className
    );

    const triggerClasses = classNames(
      `${prefix}--tooltip__label`,
      triggerClassName
    );

    const refProp = mergeRefs(this._triggerRef, ref);
    const iconProperties = { name: iconName, role: null, description: null };

    const properties = {
      role: 'button',
      tabIndex: tabIndex,
      onClick: this.handleMouse,
      onKeyDown: this.handleKeyPress,
      onMouseOver: this.handleMouse,
      onMouseOut: this.handleMouse,
      onFocus: this.handleMouse,
      onBlur: this.handleMouse,
      'aria-controls': !open ? undefined : this._tooltipId,
      'aria-expanded': open,
      'aria-describedby': open ? this._tooltipId : null,
      // if the user provides property `triggerText`,
      // then the button should use aria-labelledby to point to its id,
      // if the user doesn't provide property `triggerText`,
      // then an aria-label will be provided via the `iconDescription` property.
      ...(triggerText
        ? {
            'aria-labelledby': triggerId,
          }
        : {
            'aria-label': iconDescription,
          }),
    };

    return (
      <>
        <ClickListener onClickOutside={this.handleClickOutside}>
          {showIcon ? (

              {triggerText}
              <div
                className={`${prefix}--tooltip__trigger`}
                {...properties}
                ref={refProp}>
                <IconCustomElement {...iconProperties} />

          ) : (

              {triggerText}

          )}
        </ClickListener>
        {open && (
          <FloatingMenu
            focusTrap={focusTrap}
            selectorPrimaryFocus={this.props.selectorPrimaryFocus}
            target={this._getTarget}
            triggerRef={this._triggerRef}
            menuDirection={direction}
            menuOffset={menuOffset}
            menuRef={(node) => {
              this._tooltipEl = node;
            }}>

              <span className={`${prefix}--tooltip__caret`} />
              <div
                className={`${prefix}--tooltip__content`}
                role="dialog"
                aria-describedby={tooltipBodyId}
                aria-labelledby={triggerId}>
                {children}

          </FloatingMenu>
        )}
      </>
    );
  }
}

export { Tooltip };
export default (() => {
  const forwardRef = (props, ref) => <Tooltip {...props} innerRef={ref} />;
  forwardRef.displayName = 'Tooltip';
  return React.forwardRef(forwardRef);
})();
<|endoftext|>


<commit_before>import 'jquery.easing';

import { AutoloadPage } from 'vj/misc/PageLoader';
import CommentBox from 'vj/components/discussion/CommentBox';
import { ConfirmDialog } from 'vj/components/dialog';

import delay from 'vj/utils/delay';
import { slideDown, slideUp } from 'vj/utils/slide';
import request from 'vj/utils/request';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';

const $replyTemplate = $('.commentbox-container').eq(0).clone();

function createReplyContainer($parent) {
  const $container = $replyTemplate
    .clone()
    .hide()
    .prependTo($parent.find('.commentbox-reply-target').eq(0))
    .trigger('vjContentNew');
  return $container.find('.commentbox-placeholder');
}

async function showReplyContainer($parent) {
  const $container = $parent.find('.commentbox-container');
  // TODO: fix ugly hack. cannot get $container rect because it is invisible
  const rect = $container.parent()[0].getBoundingClientRect();
  const rectBody = document.body.getBoundingClientRect();
  if (rect.top < 100 || rect.top + 100 > window.innerHeight) {
    const targetScrollTop = rect.top - rectBody.top - window.innerHeight * 0.382;
    $('html, body').stop().animate({ scrollTop: targetScrollTop }, 400, 'easeOutCubic');
    await delay(300);
    // delay duration is set smaller than animation duration intentionally
  }
  $container.css('opacity', 0);
  await slideDown($container, 300);
  await $container.transition({ opacity: 1 }, { duration: 200 }).promise();
  $container.removeAttr('style');
}

async function destroyReplyContainer($parent) {
  const $container = $parent.find('.commentbox-container');
  $container.css('opacity', 1);
  await $container.transition({ opacity: 0 }, { duration: 200 }).promise();
  await slideUp($container, 300);
  $container.remove();
}

function onClickDummyBox(ev) {
  const $evTarget = $(ev.currentTarget);

  if (CommentBox.get($evTarget)) {
    CommentBox
      .get($evTarget)
      .focus();
    return;
  }

  const $mediaBody = $evTarget.closest('.media__body');

  const opt = {
    form: JSON.parse($evTarget.attr('data-form')),
    mode: 'comment',
    onCancel: () => {
      $mediaBody.removeClass('is-editing');
    },
  };

  $mediaBody.addClass('is-editing');

  CommentBox
    .getOrConstruct($evTarget, opt)
    .appendTo($mediaBody.find('.commentbox-placeholder').eq(0))
    .focus();
}

async function onCommentClickReplyComment(ev, options = {}) {
  const $evTarget = $(ev.currentTarget);

  if (CommentBox.get($evTarget)) {
    // If comment box is already expanded,
    // we should insert "initialText"
    CommentBox
      .get($evTarget)
      .insertText(options.initialText || '')
      .focus();
    return;
  }

  const $mediaBody = $evTarget.closest('.media__body');

  const opt = {
    initialText: '',
    mode: 'reply',
    ...options,
    onCancel: async () => {
      await destroyReplyContainer($mediaBody);
    },
  };

  const cbox = CommentBox
    .getOrConstruct($evTarget, {
      form: JSON.parse($evTarget.attr('data-form')),
      ...opt,
    })
    .appendTo(createReplyContainer($mediaBody));
  await showReplyContainer($mediaBody);
  cbox.focus();
}

async function onCommentClickReplyReply(ev) {
  const $evTarget = $(ev.currentTarget);
  const $mediaBody = $evTarget.closest('.media__body');
  const username = $mediaBody
    .find('.user-profile-name').eq(0)
    .text();

  $evTarget
    .closest('.dczcomments__item')
    .find('[name="dczcomments__op-reply-comment"]').eq(0)
    .trigger('click', { initialText: `@${username}: ` });
}

async function onCommentClickEdit(mode, ev) {
  const $evTarget = $(ev.currentTarget);

  if (CommentBox.get($evTarget)) {
    CommentBox
      .get($evTarget)
      .focus();
    return;
  }

  const $mediaBody = $evTarget.closest('.media__body');

  const raw = await request.get(
    $mediaBody.find('.typo').eq(0).attr('data-raw-url'),
    {},
    { dataType: 'text' },
  );

  const opt = {
    initialText: raw,
    form: JSON.parse($evTarget.attr('data-form')),
    mode,
    onCancel: () => {
      $mediaBody.removeClass('is-editing');
    },
  };

  $mediaBody.addClass('is-editing');

  CommentBox
    .getOrConstruct($evTarget, opt)
    .appendTo($mediaBody.find('.commentbox-edit-target').eq(0))
    .focus();
}

function onCommentClickEditComment(ev) {
  return onCommentClickEdit('comment-update', ev);
}

function onCommentClickEditReply(ev) {
  return onCommentClickEdit('reply-update', ev);
}

async function onCommentClickDelete(type, ev) {
  const message = (type === 'comment')
    ? 'Confirm deleting this comment? Its replies will be deleted as well.'
    : 'Confirm deleting this reply?';
  const action = await new ConfirmDialog({
    $body: tpl`

${i18n(message)}

`,
  }).open();
  if (action !== 'yes') {
    return;
  }

  const $evTarget = $(ev.currentTarget);
  const form = JSON.parse($evTarget.attr('data-form'));

  await request.post('', form);
  window.location.reload();
}

function onCommentClickDeleteComment(ev) {
  onCommentClickDelete('comment', ev);
}

function onCommentClickDeleteReply(ev) {
  onCommentClickDelete('reply', ev);
}

const commentsPage = new AutoloadPage('commentsPage', () => {
  $(document).on('click', '[name="dczcomments__dummy-box"]', onClickDummyBox);
  $(document).on('click', '[name="dczcomments__op-reply-comment"]', onCommentClickReplyComment);
  $(document).on('click', '[name="dczcomments__op-reply-reply"]', onCommentClickReplyReply);
  $(document).on('click', '[name="dczcomments__op-edit-comment"]', onCommentClickEditComment);
  $(document).on('click', '[name="dczcomments__op-edit-reply"]', onCommentClickEditReply);
  $(document).on('click', '[name="dczcomments__op-delete-comment"]', onCommentClickDeleteComment);
  $(document).on('click', '[name="dczcomments__op-delete-reply"]', onCommentClickDeleteReply);
});

export default commentsPage;
<commit_msg>fix username trim (#578)<commit_after>import 'jquery.easing';

import { AutoloadPage } from 'vj/misc/PageLoader';
import CommentBox from 'vj/components/discussion/CommentBox';
import { ConfirmDialog } from 'vj/components/dialog';

import delay from 'vj/utils/delay';
import { slideDown, slideUp } from 'vj/utils/slide';
import request from 'vj/utils/request';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';

const $replyTemplate = $('.commentbox-container').eq(0).clone();

function createReplyContainer($parent) {
  const $container = $replyTemplate
    .clone()
    .hide()
    .prependTo($parent.find('.commentbox-reply-target').eq(0))
    .trigger('vjContentNew');
  return $container.find('.commentbox-placeholder');
}

async function showReplyContainer($parent) {
  const $container = $parent.find('.commentbox-container');
  // TODO: fix ugly hack. cannot get $container rect because it is invisible
  const rect = $container.parent()[0].getBoundingClientRect();
  const rectBody = document.body.getBoundingClientRect();
  if (rect.top < 100 || rect.top + 100 > window.innerHeight) {
    const targetScrollTop = rect.top - rectBody.top - window.innerHeight * 0.382;
    $('html, body').stop().animate({ scrollTop: targetScrollTop }, 400, 'easeOutCubic');
    await delay(300);
    // delay duration is set smaller than animation duration intentionally
  }
  $container.css('opacity', 0);
  await slideDown($container, 300);
  await $container.transition({ opacity: 1 }, { duration: 200 }).promise();
  $container.removeAttr('style');
}

async function destroyReplyContainer($parent) {
  const $container = $parent.find('.commentbox-container');
  $container.css('opacity', 1);
  await $container.transition({ opacity: 0 }, { duration: 200 }).promise();
  await slideUp($container, 300);
  $container.remove();
}

function onClickDummyBox(ev) {
  const $evTarget = $(ev.currentTarget);

  if (CommentBox.get($evTarget)) {
    CommentBox
      .get($evTarget)
      .focus();
    return;
  }

  const $mediaBody = $evTarget.closest('.media__body');

  const opt = {
    form: JSON.parse($evTarget.attr('data-form')),
    mode: 'comment',
    onCancel: () => {
      $mediaBody.removeClass('is-editing');
    },
  };

  $mediaBody.addClass('is-editing');

  CommentBox
    .getOrConstruct($evTarget, opt)
    .appendTo($mediaBody.find('.commentbox-placeholder').eq(0))
    .focus();
}

async function onCommentClickReplyComment(ev, options = {}) {
  const $evTarget = $(ev.currentTarget);

  if (CommentBox.get($evTarget)) {
    // If comment box is already expanded,
    // we should insert "initialText"
    CommentBox
      .get($evTarget)
      .insertText(options.initialText || '')
      .focus();
    return;
  }

  const $mediaBody = $evTarget.closest('.media__body');

  const opt = {
    initialText: '',
    mode: 'reply',
    ...options,
    onCancel: async () => {
      await destroyReplyContainer($mediaBody);
    },
  };

  const cbox = CommentBox
    .getOrConstruct($evTarget, {
      form: JSON.parse($evTarget.attr('data-form')),
      ...opt,
    })
    .appendTo(createReplyContainer($mediaBody));
  await showReplyContainer($mediaBody);
  cbox.focus();
}

async function onCommentClickReplyReply(ev) {
  const $evTarget = $(ev.currentTarget);
  const $mediaBody = $evTarget.closest('.media__body');
  const username = $mediaBody
    .find('.user-profile-name').eq(0)
    .text().trim();

  $evTarget
    .closest('.dczcomments__item')
    .find('[name="dczcomments__op-reply-comment"]').eq(0)
    .trigger('click', { initialText: `@${username}: ` });
}

async function onCommentClickEdit(mode, ev) {
  const $evTarget = $(ev.currentTarget);

  if (CommentBox.get($evTarget)) {
    CommentBox
      .get($evTarget)
      .focus();
    return;
  }

  const $mediaBody = $evTarget.closest('.media__body');

  const raw = await request.get(
    $mediaBody.find('.typo').eq(0).attr('data-raw-url'),
    {},
    { dataType: 'text' },
  );

  const opt = {
    initialText: raw,
    form: JSON.parse($evTarget.attr('data-form')),
    mode,
    onCancel: () => {
      $mediaBody.removeClass('is-editing');
    },
  };

  $mediaBody.addClass('is-editing');

  CommentBox
    .getOrConstruct($evTarget, opt)
    .appendTo($mediaBody.find('.commentbox-edit-target').eq(0))
    .focus();
}

function onCommentClickEditComment(ev) {
  return onCommentClickEdit('comment-update', ev);
}

function onCommentClickEditReply(ev) {
  return onCommentClickEdit('reply-update', ev);
}

async function onCommentClickDelete(type, ev) {
  const message = (type === 'comment')
    ? 'Confirm deleting this comment? Its replies will be deleted as well.'
    : 'Confirm deleting this reply?';
  const action = await new ConfirmDialog({
    $body: tpl`

${i18n(message)}

`,
  }).open();
  if (action !== 'yes') {
    return;
  }

  const $evTarget = $(ev.currentTarget);
  const form = JSON.parse($evTarget.attr('data-form'));

  await request.post('', form);
  window.location.reload();
}

function onCommentClickDeleteComment(ev) {
  onCommentClickDelete('comment', ev);
}

function onCommentClickDeleteReply(ev) {
  onCommentClickDelete('reply', ev);
}

const commentsPage = new AutoloadPage('commentsPage', () => {
  $(document).on('click', '[name="dczcomments__dummy-box"]', onClickDummyBox);
  $(document).on('click', '[name="dczcomments__op-reply-comment"]', onCommentClickReplyComment);
  $(document).on('click', '[name="dczcomments__op-reply-reply"]', onCommentClickReplyReply);
  $(document).on('click', '[name="dczcomments__op-edit-comment"]', onCommentClickEditComment);
  $(document).on('click', '[name="dczcomments__op-edit-reply"]', onCommentClickEditReply);
  $(document).on('click', '[name="dczcomments__op-delete-comment"]', onCommentClickDeleteComment);
  $(document).on('click', '[name="dczcomments__op-delete-reply"]', onCommentClickDeleteReply);
});

export default commentsPage;
<|endoftext|>


<commit_before>'use strict';

var config = {
  directory : './.cache-test'
};

var fs = require('fs-extra'),
pkg = require('../package.json'),
should = require('should'),
_ = require('underscore');

function removeCacheDir(done){
 fs.remove(config.directory, done);
}

before(removeCacheDir);

describe('versioning', function(){
  var cache = require('..')(config);

  it('should have a version', function(){
    cache.should.have.property('version');
  });

  it('should equal package version', function(){
    cache.version.should.be.exactly(pkg.version);
  });
});

describe('cachy storage interface exists', function(){
  var cache = require('..')(config);

  var fxns = ['write', 'has', 'read', 'remove', 'clear', 'keys'];
  describe('interface has', function(){
    _.each(fxns, function(fxn){
      it(fxn + '()', function(){
	cache[fxn].should.be.a.Function;
      });
    });
  });
});

describe('directory creation', function(){
  before(removeCacheDir);

  it('should not have the cache directory', function(done){
    fs.exists(config.directory, function(exists){
      exists.should.be.false;
      done();
    });
  });

  it('should create the cache directory on write', function(done){
    var cache = require('..')(config);

    cache.write('key', 'value', function(err){
      if(err) return done(err);

      fs.exists(config.directory, function(exists){
	exists.should.be.true;
	done();
      });
    });
  });

  after(removeCacheDir);
});

describe('operations', function(done){

  before(removeCacheDir);

  var cache = require('..')(config);
  var samples = [
    {key : 'some-string', data : 'This is a plain string'},
    {key : 'some-number', data : 12345678},
    {key : 'an-object', data : {first : 'first item', second : 12345}}];

  var keys = _.pluck(samples, 'key');

  describe('write()', function(){
    _.each(samples, function(item){
      it('should write ' + item.key, function(done){
	cache.write(item.key, item.data, done);
      });
    });
  });

  describe('read()', function(){
    _.each(samples, function(item){
      it('should read ' + item.key, function(done){
	cache.read(item.key, function(err, data){
	  if(err) return done(err);
	  data.should.eql(item.data);
	  return done();
	});
      });
    });
  });

  describe('keys()', function(){
    it('should retrieve matching keys', function(done){
      cache.keys(function(err, results){

	if(err) return done(err);

	results.should.have.length(keys.length);
	_.each(keys, function(key){
	  _.contains(keys, key).should.be.true;
	});
      });
      return done();
    });
  });

  describe('size()', function(){
    it('should match', function(done){
      cache.size(function(size){
	size.should.be.a.Number;
	size.should.equal(samples.length);
	done();
      });
    });
  });

  describe('has()', function(){
    _.each(keys, function(key){
      it('should have key "' + key + '"', function(done){
	cache.has(key, function(has){
	  has.should.be.true;
	  done();
	});
      });
    });

    _.each(['badkey', 'nonexistkey', '120394'], function(key){
      it('should not have key "' + key + '"', function(done){
	cache.has(key, function(has){
	  has.should.be.false;
	  done();
	});
      });
    });
  });

  describe('remove()', function(){

    it('should remove key "' + keys[0] + '"', function(done){
      cache.remove(keys[0], done);
    });

    it('should not have key "' + keys[0] + '"', function(done){
      cache.has(keys[0], function(has){
	has.should.be.false;
	done();
      });
    });

    it('should still have key "' + keys[1] + '"', function(done){
      cache.has(keys[1], function(has){
	has.should.be.true;
	done();
      });
    });
  });

  describe('clear()', function(){
    it('should clear cache', function(done){
      cache.clear(function(err){
	if(err) return done(err);
	cache.size(function(size){
	  size.should.equal(0);
	  done();
	});
      });
    });
  });

  after(removeCacheDir);
});

/* add more tests */

after(removeCacheDir);
<commit_msg>added size() to interface test<commit_after>'use strict';

var config = {
  directory : './.cache-test'
};

var fs = require('fs-extra'),
pkg = require('../package.json'),
should = require('should'),
_ = require('underscore');

function removeCacheDir(done){
 fs.remove(config.directory, done);
}

before(removeCacheDir);

describe('versioning', function(){
  var cache = require('..')(config);

  it('should have a version', function(){
    cache.should.have.property('version');
  });

  it('should equal package version', function(){
    cache.version.should.be.exactly(pkg.version);
  });
});

describe('cachy storage interface exists', function(){
  var cache = require('..')(config);

  var fxns = ['write', 'has', 'read', 'remove', 'clear', 'keys', 'size'];
  describe('interface has', function(){
    _.each(fxns, function(fxn){
      it(fxn + '()', function(){
	cache[fxn].should.be.a.Function;
      });
    });
  });
});

describe('directory creation', function(){
  before(removeCacheDir);

  it('should not have the cache directory', function(done){
    fs.exists(config.directory, function(exists){
      exists.should.be.false;
      done();
    });
  });

  it('should create the cache directory on write', function(done){
    var cache = require('..')(config);

    cache.write('key', 'value', function(err){
      if(err) return done(err);

      fs.exists(config.directory, function(exists){
	exists.should.be.true;
	done();
      });
    });
  });

  after(removeCacheDir);
});

describe('operations', function(done){

  before(removeCacheDir);

  var cache = require('..')(config);
  var samples = [
    {key : 'some-string', data : 'This is a plain string'},
    {key : 'some-number', data : 12345678},
    {key : 'an-object', data : {first : 'first item', second : 12345}}];

  var keys = _.pluck(samples, 'key');

  describe('write()', function(){
    _.each(samples, function(item){
      it('should write ' + item.key, function(done){
	cache.write(item.key, item.data, done);
      });
    });
  });

  describe('read()', function(){
    _.each(samples, function(item){
      it('should read ' + item.key, function(done){
	cache.read(item.key, function(err, data){
	  if(err) return done(err);
	  data.should.eql(item.data);
	  return done();
	});
      });
    });
  });

  describe('keys()', function(){
    it('should retrieve matching keys', function(done){
      cache.keys(function(err, results){

	if(err) return done(err);

	results.should.have.length(keys.length);
	_.each(keys, function(key){
	  _.contains(keys, key).should.be.true;
	});
      });
      return done();
    });
  });

  describe('size()', function(){
    it('should match', function(done){
      cache.size(function(size){
	size.should.be.a.Number;
	size.should.equal(samples.length);
	done();
      });
    });
  });

  describe('has()', function(){
    _.each(keys, function(key){
      it('should have key "' + key + '"', function(done){
	cache.has(key, function(has){
	  has.should.be.true;
	  done();
	});
      });
    });

    _.each(['badkey', 'nonexistkey', '120394'], function(key){
      it('should not have key "' + key + '"', function(done){
	cache.has(key, function(has){
	  has.should.be.false;
	  done();
	});
      });
    });
  });

  describe('remove()', function(){

    it('should remove key "' + keys[0] + '"', function(done){
      cache.remove(keys[0], done);
    });

    it('should not have key "' + keys[0] + '"', function(done){
      cache.has(keys[0], function(has){
	has.should.be.false;
	done();
      });
    });

    it('should still have key "' + keys[1] + '"', function(done){
      cache.has(keys[1], function(has){
	has.should.be.true;
	done();
      });
    });
  });

  describe('clear()', function(){
    it('should clear cache', function(done){
      cache.clear(function(err){
	if(err) return done(err);
	cache.size(function(size){
	  size.should.equal(0);
	  done();
	});
      });
    });
  });

  after(removeCacheDir);
});

/* add more tests */

after(removeCacheDir);
<|endoftext|>


<commit_before>let browser, chai, noflo, path;
if ((typeof process !== 'undefined') && process.execPath && process.execPath.match(/node|iojs/)) {
  if (!chai) { chai = require('chai'); }
  noflo = require('../src/lib/NoFlo');
  path = require('path');
  browser = false;
} else {
  noflo = require('noflo');
  browser = true;
}

describe('NoFlo interface', function() {
  it('should be able to tell whether it is running on browser', function() {
    chai.expect(noflo.isBrowser()).to.equal(browser);
  });
  describe('working with graph files', function() {
    let targetPath = null;
    before(function() {
      // These features only work on Node.js
      if (noflo.isBrowser()) {
        this.skip();
        return;
      }
      targetPath = path.resolve(__dirname, 'tmp.json');
    });
    after(function(done) {
      if (noflo.isBrowser()) {
        done();
        return;
      }
      const fs = require('fs');
      fs.unlink(targetPath, done);
    });
    it('should be able to save a graph file', function(done) {
      const graph = new noflo.Graph;
      graph.addNode('G', 'Graph');
      noflo.saveFile(graph, targetPath, done);
    });
    it('should be able to load a graph file', function(done) {
      noflo.loadFile(targetPath, {
        baseDir: process.cwd(),
        delay: true
      }
      , function(err, network) {
        if (err) {
          done(err);
          return;
        }
        chai.expect(network.isRunning()).to.equal(false);
        done();
      });
    });
  });
});
<commit_msg>decaffeinate: Run post-processing cleanups on NoFlo.coffee<commit_after>/* eslint-disable
    func-names,
    global-require,
    import/no-extraneous-dependencies,
    import/no-unresolved,
    no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
let browser; let chai; let noflo; let
  path;
if ((typeof process !== 'undefined') && process.execPath && process.execPath.match(/node|iojs/)) {
  if (!chai) { chai = require('chai'); }
  noflo = require('../src/lib/NoFlo');
  path = require('path');
  browser = false;
} else {
  noflo = require('noflo');
  browser = true;
}

describe('NoFlo interface', () => {
  it('should be able to tell whether it is running on browser', () => {
    chai.expect(noflo.isBrowser()).to.equal(browser);
  });
  describe('working with graph files', () => {
    let targetPath = null;
    before(function () {
      // These features only work on Node.js
      if (noflo.isBrowser()) {
        this.skip();
        return;
      }
      targetPath = path.resolve(__dirname, 'tmp.json');
    });
    after((done) => {
      if (noflo.isBrowser()) {
        done();
        return;
      }
      const fs = require('fs');
      fs.unlink(targetPath, done);
    });
    it('should be able to save a graph file', (done) => {
      const graph = new noflo.Graph();
      graph.addNode('G', 'Graph');
      noflo.saveFile(graph, targetPath, done);
    });
    it('should be able to load a graph file', (done) => {
      noflo.loadFile(targetPath, {
        baseDir: process.cwd(),
        delay: true,
      },
      (err, network) => {
        if (err) {
          done(err);
          return;
        }
        chai.expect(network.isRunning()).to.equal(false);
        done();
      });
    });
  });
});
<|endoftext|>


<commit_before>const fs = require('fs');
const path = require('path');
const resolveApp = require('rear-core/resolve-app');

const ownNodeModules = fs.realpathSync(
  path.join(__dirname, '..', 'node_modules')
);

const AppPaths = {
  ownNodeModules,
  root: resolveApp('src'),
  appIndexJs: resolveApp('src', 'index.js'),
  // TODO: rename to appBuild ?
  dest: resolveApp('lib'),
  eslintConfig: path.join(ownNodeModules, 'eslint-config-rear', 'index.js'),
  flowBin:  path.join(ownNodeModules, 'flow-bin'),
  eslintBin: path.join(ownNodeModules, 'eslint'),
  appNodeModules: resolveApp('node_modules'),
}

module.exports = AppPaths;
<commit_msg>Fix an issue that prevented modules to be resolved<commit_after>const fs = require('fs');
const path = require('path');
const resolveApp = require('rear-core/resolve-app');

let ownNodeModules;

try {
  // Since this package is also used to build other packages
  // in this repo, we need to make sure its dependencies
  // are available when is symlinked by lerna.
  // Normally, modules are resolved from the app's node_modules.
  ownNodeModules = fs.realpathSync(
    path.join(__dirname, '..', 'node_modules')
  );
} catch (err) {
  ownNodeModules = resolveApp('node_modules');
}

const AppPaths = {
  ownNodeModules,
  root: resolveApp('src'),
  appIndexJs: resolveApp('src', 'index.js'),
  dest: resolveApp('lib'),
  eslintConfig: path.join(ownNodeModules, 'eslint-config-rear', 'index.js'),
  flowBin:  path.join(ownNodeModules, 'flow-bin'),
  eslintBin: path.join(ownNodeModules, 'eslint'),
  appNodeModules: resolveApp('node_modules'),
}

module.exports = AppPaths;
<|endoftext|>


<commit_before>#!/usr/bin/env node
/**
 * Wallpaper Shuffle
 * A daemon that randomly rotates wallpaper
 */

/* jshint node: true */

'use strict';

var _       = require('lodash');
var Q       = require('q');
var io      = require('q-io/fs');
var os      = require('os');
var path    = require('path');
var glob    = require('glob');
var pkg     = require(path.join(__dirname, '..', 'package.json'));
var chalk   = require('chalk');
var moment  = require('moment');
var child   = require('child_process');
var argv    = require('yargs')
  .command('start', 'Begin rotating wallpapers')
  .command('stop', 'Stop rotating wallpapers')
  .command('status', 'Checks for a running wallpaper-shuffle process')
  .command('next', 'Manually switch to a new random wallpaper')
  .option('d', {
    description: 'Directory containing images',
    default: process.cwd(),
    alias: 'directory'
  })
  .option('i', {
    description: 'Transition interval (ex. "5 minutes", "1 hour")',
    default: '5 minutes',
    alias: 'interval'
  })
  .option('g', {
    description: 'Pattern of files to search for',
    default: '*.{png,jpg,jpeg}',
    alias: 'glob'
  })
  .option('p', {
    description: 'PID file',
    default: path.join(os.tmpdir(), 'wallpaper-shuffle.pid'),
    alias: 'pid'
  })
  .option('h', { alias: 'help' })
  .help('help')
  .option('v', { alias: 'version' })
  .version(pkg.version, 'version', 'Display version information')
  .example('$0 start -d ~/Photos/Wallpaper -i "30 minutes"')
  .argv;

var daemonScript = path.join(__dirname, '..', 'lib', 'daemon.js');

var isRunning = function() {
  return io.exists(argv.pid);
};

var getPID = function() {
  return io.read(argv.pid).then(function(pid) {
    return parseInt(pid, 10);
  });
}

var countWallpapers = function(pathGlob) {
  var deferred = Q.defer();

  glob(pathGlob, function(err, matches) {
    if (err) {
      deferred.resolve(0);
    } else {
      deferred.resolve(matches.length);
    }
  });

  return deferred.promise;
};

var actions = {

  start: function() {
    var timeSplit    = argv.interval.split(' ');
    var timeNum      = parseFloat(timeSplit[0]);
    var timeUnits    = timeSplit.slice(1).join(' ');
    var interval     = moment.duration(timeNum, timeUnits);
    var milliseconds = interval.asMilliseconds();
    var pathGlob     = path.join(argv.directory, argv.glob);

    if (_.isEmpty(glob.sync(pathGlob))) {
      console.log(chalk.bold.red('No wallpapers found: ') + argv.directory);
      process.exit(1);
    }

    return countWallpapers(pathGlob).then(function(count) {
      if (!count) {
        throw new Error('No wallpapers found!');
      }
    }).then(function() {
      var daemon = child.spawn(daemonScript, [], {
        env: process.env,
        stdio: [ 'ignore', 'ignore', 'ignore', 'ipc' ],
        detached: true
      });

      return io.write(argv.pid, String(daemon.pid))
    }, function(err) {
      console.log(chalk.bold.red(err.message));
      process.exit(1);
    }).then(function(pid) {
      daemon.on('message', function(status) {
        if (status.running) {
          console.log(chalk.bold.green('running ') + '(pid: %s)', pid);
          process.exit(0);
        }

        console.log(chalk.bold.red('error starting daemon'));
        process.exit(1);
      });

      daemon.send({ pattern: pathGlob, interval: milliseconds });
      daemon.unref();
    });
  },

  stop: function() {
    return isRunning().then(function(exists) {
      if (!exists) {
        console.log(chalk.bold.red('not running'));
        process.exit(1);
      }

      return getPID();
    }).then(function(pid) {
      process.kill(pid, 'SIGTERM');
      return io.remove(argv.pid);
    }).then(function() {
      console.log(chalk.magenta.bold('stopped'));
      process.exit(0);
    });
  },

  pause: function() {
    return isRunning().then(function(exists) {
      if (!exists) {
        console.log(chalk.bold.red('not running'));
        process.exit(1);
      }

      return getPID();
    }).then(function(pid) {
      process.kill(pid, 'SIGINT');
      console.log(chalk.bold.green('play/pause'));
      process.exit();
    });
  },

  next: function() {
    return isRunning().then(function(exists) {
      if (!exists) {
        console.log(chalk.bold.red('not running'));
        process.exit(1);
      }

      return getPID();
    }).then(function(pid) {
      process.kill(pid, 'SIGUSR2');
      console.log(chalk.bold.green('changing wallpaper'));
      process.exit(0);
    });
  },

  status: function() {
    return isRunning().then(function(exists) {
      if (exists) {
        return getPID();
      }
      throw new Error('not running');
    }).then(function(pid) {
      console.log(chalk.bold.green('running ') + '(pid %d)', pid);
    }, function(err) {
      console.log(chalk.bold.red(err.message));
    }).done(function() { process.exit(); });
  }

};

if (argv._.length === 0) {
  console.log(chalk.bold.red('Please give me a command!'));
  process.exit(1);
}

var command = argv._[0];

if (!actions.hasOwnProperty(command)) {
  console.log(chalk.red.bold('invalid command!'));
  process.exit(1);
}

actions[command].call(this);
<commit_msg>removed synchronous code from start()<commit_after>#!/usr/bin/env node
/**
 * Wallpaper Shuffle
 * A daemon that randomly rotates wallpaper
 */

/* jshint node: true */

'use strict';

var _       = require('lodash');
var Q       = require('q');
var io      = require('q-io/fs');
var os      = require('os');
var path    = require('path');
var glob    = require('glob');
var pkg     = require(path.join(__dirname, '..', 'package.json'));
var chalk   = require('chalk');
var moment  = require('moment');
var child   = require('child_process');
var argv    = require('yargs')
  .command('start', 'Begin rotating wallpapers')
  .command('stop', 'Stop rotating wallpapers')
  .command('status', 'Checks for a running wallpaper-shuffle process')
  .command('next', 'Manually switch to a new random wallpaper')
  .option('d', {
    description: 'Directory containing images',
    default: process.cwd(),
    alias: 'directory'
  })
  .option('i', {
    description: 'Transition interval (ex. "5 minutes", "1 hour")',
    default: '5 minutes',
    alias: 'interval'
  })
  .option('g', {
    description: 'Pattern of files to search for',
    default: '*.{png,jpg,jpeg}',
    alias: 'glob'
  })
  .option('p', {
    description: 'PID file',
    default: path.join(os.tmpdir(), 'wallpaper-shuffle.pid'),
    alias: 'pid'
  })
  .option('h', { alias: 'help' })
  .help('help')
  .option('v', { alias: 'version' })
  .version(pkg.version, 'version', 'Display version information')
  .example('$0 start -d ~/Photos/Wallpaper -i "30 minutes"')
  .argv;

var daemonScript = path.join(__dirname, '..', 'lib', 'daemon.js');

var isRunning = function() {
  return io.exists(argv.pid);
};

var getPID = function() {
  return io.read(argv.pid).then(function(pid) {
    return parseInt(pid, 10);
  });
}

var countWallpapers = function(pathGlob) {
  var deferred = Q.defer();

  glob(pathGlob, function(err, matches) {
    if (err) {
      deferred.resolve(0);
    } else {
      deferred.resolve(matches.length);
    }
  });

  return deferred.promise;
};

var actions = {

  start: function() {
    var timeSplit    = argv.interval.split(' ');
    var timeNum      = parseFloat(timeSplit[0]);
    var timeUnits    = timeSplit.slice(1).join(' ');
    var interval     = moment.duration(timeNum, timeUnits);
    var milliseconds = interval.asMilliseconds();
    var pathGlob     = path.join(argv.directory, argv.glob);

    return countWallpapers(pathGlob).then(function(count) {
      if (!count) {
        throw new Error('No wallpapers found!');
      }
    }).then(function() {
      var daemon = child.spawn(daemonScript, [], {
        env: process.env,
        stdio: [ 'ignore', 'ignore', 'ignore', 'ipc' ],
        detached: true
      });

      return io.write(argv.pid, String(daemon.pid))
    }, function(err) {
      console.log(chalk.bold.red(err.message));
      process.exit(1);
    }).then(function(pid) {
      daemon.on('message', function(status) {
        if (status.running) {
          console.log(chalk.bold.green('running ') + '(pid: %s)', pid);
          process.exit(0);
        }

        console.log(chalk.bold.red('error starting daemon'));
        process.exit(1);
      });

      daemon.send({ pattern: pathGlob, interval: milliseconds });
      daemon.unref();
    });
  },

  stop: function() {
    return isRunning().then(function(exists) {
      if (!exists) {
        console.log(chalk.bold.red('not running'));
        process.exit(1);
      }

      return getPID();
    }).then(function(pid) {
      process.kill(pid, 'SIGTERM');
      return io.remove(argv.pid);
    }).then(function() {
      console.log(chalk.magenta.bold('stopped'));
      process.exit(0);
    });
  },

  pause: function() {
    return isRunning().then(function(exists) {
      if (!exists) {
        console.log(chalk.bold.red('not running'));
        process.exit(1);
      }

      return getPID();
    }).then(function(pid) {
      process.kill(pid, 'SIGINT');
      console.log(chalk.bold.green('play/pause'));
      process.exit();
    });
  },

  next: function() {
    return isRunning().then(function(exists) {
      if (!exists) {
        console.log(chalk.bold.red('not running'));
        process.exit(1);
      }

      return getPID();
    }).then(function(pid) {
      process.kill(pid, 'SIGUSR2');
      console.log(chalk.bold.green('changing wallpaper'));
      process.exit(0);
    });
  },

  status: function() {
    return isRunning().then(function(exists) {
      if (exists) {
        return getPID();
      }
      throw new Error('not running');
    }).then(function(pid) {
      console.log(chalk.bold.green('running ') + '(pid %d)', pid);
    }, function(err) {
      console.log(chalk.bold.red(err.message));
    }).done(function() { process.exit(); });
  }

};

if (argv._.length === 0) {
  console.log(chalk.bold.red('Please give me a command!'));
  process.exit(1);
}

var command = argv._[0];

if (!actions.hasOwnProperty(command)) {
  console.log(chalk.red.bold('invalid command!'));
  process.exit(1);
}

actions[command].call(this);
<|endoftext|>