<commit_before>import {
  EventEmitter,
  Queue,
} from '@theclinician/toolbelt';
import Socket from './socket';
import Method from './Method.js';
import Subscription from './Subscription.js';
import {
  once,
  omit,
  uniqueId,
} from './utils';
import {
  DDPError,
} from './errors.js';
import * as multiStorage from './multiStorage.js';

const DDP_VERSION = '1';
const DEFAULT_RECONNECT_INTERVAL = 10000;

interface AsyncStorage {
  get(string): Promise;
  set(string, mixed): Promise;
  del(string): Promise;
}

class DDP extends EventEmitter {
  status: string;
  subscriptions: { [string]: Subscription };
  methods: { [string]: Method };
  socket: Socket;
  methodsQueue: Queue;
  collections: { [string]: { [string]: mixed } };
  models: { [string]: Function };
  storage: AsyncStorage;

  static registerModel(Model, collection) {
    if (!collection) {
      throw Error('Method registerModel() requires collection name as the second argument');
    }
    if (this.models[collection]) {
      console.error(`Overwriting model ${this.models[collection].name} bound to collection ${collection}; new model is ${Model.name}`);
    }
    this.models[Model.collection] = Model;
  }

  constructor(options: {
    debug?: boolean,
    endpoint: string,
    autoConnect?: boolean,
    autoReconnect?: boolean,
    reconnectInterval?: number,
    storage?: AsyncStorage,
  }) {
    super();

    this.captureUnhandledErrors([
      'error',
      'loginError',
      'resumeLoginError',
      'logoutError',
    ]);

    this.status = 'disconnected';
    this.endpoint = options.endpoint;

    // Default `autoConnect` and `autoReconnect` to true
    this.autoConnect = (options.autoConnect !== false);
    this.autoReconnect = (options.autoReconnect !== false);
    this.reconnectInterval = options.reconnectInterval || DEFAULT_RECONNECT_INTERVAL;
    this.collections = {};
    this.storage = options.storage || multiStorage;

    // Methods/ subscriptions handlers
    this.subscriptions = {};
    this.methods = {};

    this.methodsQueue = new Queue({
      onError: () => false, // make sure the queue is not terminated even if one error occurs
    });
    this.methodsQueue.pause();

    // Socket
    this.socket = new Socket(options.SocketConstructor, options.endpoint);

    if (options.debug) {
      this.socket.on('message:out', message => console.warn('DDP/OUT', message));
      this.socket.on('message:in', message => console.warn('DDP/IN', message));
    }

    this.socket.on('open', () => {
      // When the socket opens, send the `connect` message
      // to establish the DDP connection
      this.socket.send({
        msg: 'connect',
        version: DDP_VERSION,
        support: [DDP_VERSION],
      });
    });

    this.socket.on('close', () => {
      this.status = 'disconnected';

      this.methodsQueue.pause();
      this.methodsQueue.clear();
      this.cancelPendingMethods(new Error('Connection was lost'));

      this.emit('disconnected');
      if (this.autoReconnect) {
        // Schedule a connection
        setTimeout(
          this.socket.open.bind(this.socket),
          this.reconnectInterval,
        );
      }
    });

    this.socket.on('message:in', (message) => {
      switch (message.msg) {
        case 'connected':
          this.status = 'connected';
          this.resumeLogin().then(() => {
            this.methodsQueue.resume();
            this.restoreSubscriptions();
            this.emit('connected');
          });
          break;
        case 'ping':
          // Reply with a `pong` message to prevent the server from
          // closing the connection
          this.socket.send({ msg: 'pong', id: message.id });
          break;
        case 'ready':
          message.subs.forEach(id => this.subscriptions[id] && this.subscriptions[id].ready());
          break;
        case 'nosub':
          if (this.subscriptions[message.id]) {
            this.subscriptions[message.id].nosub(message.error);
          }
          break;
        case 'result':
          if (this.methods[message.id]) {
            const { error, result } = message;
            this.methods[message.id].result({
              error,
              result,
            });
          }
          break;
        case 'updated':
          message.methods.forEach(id => this.methods[id] && this.methods[id].updated());
          break;
        case 'error':
          this.emit('error', new DDPError('DDPError', message.reason));
          break;
        case 'added':
        case 'changed':
        case 'removed':
          this[message.msg](message);
          break;
        default:
          // ignore the message ...
      }
    });

    if (this.autoConnect) {
      this.connect();
    }
  }

  connect() {
    this.socket.open();
  }

  added({ collection, id, fields }) {
    const Model = this.constructor.models[collection] || this.constructor.UnknownModel;
    if (Model) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: new Model({
            _id: id,
            ...fields,
          }),
        },
      };
      this.emit('dataUpdated', this.collections);
    }
    this.emit('added', { collection, id, fields });
  }

  changed({ collection, id, fields, cleared }) {
    const Model = this.constructor.models[collection] || this.constructor.UnknownModel;
    if (Model) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: new Model(omit({
            ...this.collections[collection][id],
            ...fields,
          }, cleared)),
        },
      };
      this.emit('dataUpdated', this.collections);
    }
    this.emit('changed', { collection, id, fields });
  }

  removed({ collection, id }) {
    const Model = this.constructor.models[collection] || this.constructor.UnknownModel;
    if (Model) {
      this.collections = {
        ...this.collections,
        [collection]: this.collections[collection]
          ? omit(this.collections[collection], [id])
          : {},
      };
      this.emit('dataUpdated', this.collections);
    }
    this.emit('removed', { collection, id });
  }

  disconnect() {
    /**
     * If `disconnect` is called, the caller likely doesn't want the
     * the instance to try to auto-reconnect. Therefore we set the
     * `autoReconnect` flag to false.
     */
    this.autoReconnect = false;
    this.socket.close();
  }

  cancelPendingMethods(err) {
    Object.keys(this.methods).forEach((id) => {
      this.methods[id].cancel(err);
    });
  }

  restoreSubscriptions() {
    let numberOfPending = Object.keys(this.subscriptions).length;

    const cb = () => {
      numberOfPending -= 1;
      if (numberOfPending === 0) {
        this.emit('restored');
      }
    };

    if (numberOfPending > 0 && this.status === 'connected') {
      this.emit('restoring');

      Object.keys(this.subscriptions).forEach((id) => {
        this.subscriptions[id].setCallback(cb);
        this.socket.send(this.subscriptions[id].toDDPMessage(id));
      });
    }
  }

  subscribe(name, params, { onStop, onReady } = {}) {
    const id = uniqueId();

    this.subscriptions[id] = new Subscription({
      name,
      params,
      onStop,
      onReady,
    });

    if (this.status === 'connected') {
      this.socket.send(this.subscriptions[id].toDDPMessage(id));
    }

    return {
      stop: once(() => {
        if (this.status === 'connected') {
          this.socket.send({ id, msg: 'unsub' });
        }
        delete this.subscriptions[id];
        if (onStop) {
          onStop();
        }
      }),
    };
  }

  apply(name, params, {
    wait,
    noRetry,
    skipQueue,
    onResultReceived,
    throwStubExceptions,
  } = {}, asyncCallback) {
    if (!asyncCallback) {
      return new Promise((resolve, reject) => {
        this.apply(name, params, {
          wait,
          noRetry,
          skipQueue,
          onResultReceived,
          throwStubExceptions,
        }, (error, result) => (error ? reject(error) : resolve(result)));
      });
    }

    const id = uniqueId();

    this.methods[id] = new Method({
      name,
      params,
      noRetry,
      onResultReceived,
      asyncCallback,
    });

    const action = (cb) => {
      const method = this.methods[id];
      if (method) {
        this.methods[id].setCallback((error, result) => {
          if (cb) {
            cb(error, result);
          }
          delete this.methods[id];
        });
        if (this.status === 'connected') {
          this.socket.send(method.toDDPMessage(id));
        } else {
          // NOTE: Theoretically this should not happen since we are pausing
          //       queue when status in disconnected. Better safe than sorry.
          this.methods[id].cancel();
        }
      }
    };

    if (skipQueue) {
      action(null);
    } else {
      this.methodsQueue.push({
        noWait: !wait,
        onStop: (err) => {
          // This can be called when queue is clear, e.g. when connection is lost.
          if (this.methods[id]) {
            this.methods[id].cancel(err);
          }
        },
        action,
      });
    }

    return undefined;
  }

  handleLogin({ id, token }) {
    this.userId = id;
    return this.storage
          .set(`${this.endpoint}__login_token__`, token)
          .then(this.emit.bind(this, 'loggedIn', id))
          .then(() => id);
  }

  handleLogout() {
    this.userId = null;
    return this.storage
          .del(`${this.endpoint}__login_token__`)
          .then(this.emit.bind(this, 'loggedOut'))
          .then(() => null);
  }

  resumeLogin() {
    // NOTE: This promise never rejects
    return Promise.resolve()
          .then(this.emit.bind(this, 'loggingIn'))
          .then(() => this.storage.get(`${this.endpoint}__login_token__`))
          .then((resume) => {
            if (!resume) {
              // NOTE: It's important to emit loginError here, because we've already
              //       triggered loggingIn. So by triggering error as well we are
              //       indicating that the login procedure was actually terminated.
              this.emit('loginError', new Error('No login token'));
              return undefined;
            }
            return this.login({ resume }, { skipQueue: true });
          })
          .catch(err =>
            Promise.resolve()
              .then(this.handleLogout.bind(this))
              .then(this.emit.bind(this, 'resumeLoginError', err)),
          );
  }

  login(options, { skipQueue } = {}) {
    return this.executeLoginRoutine('login', [options], { skipQueue });
  }

  logout() {
    return Promise.resolve()
          .then(this.emit.bind(this, 'loggingIn'))
          .then(this.apply.bind(this, 'logout', []))
          .then(this.handleLogout.bind(this))
          .catch(err =>
            Promise.resolve()
              .then(this.handleLogout.bind(this))
              .then(this.emit.bind(this, 'logoutError', err))
              .then(() => Promise.reject(err)),
          );
  }

  executeLoginRoutine(name, params, { skipQueue = false } = {}) {
    return Promise.resolve()
          .then(this.emit.bind(this, 'loggingIn'))
          .then(this.apply.bind(this, name, params, { skipQueue, wait: true }))
          .then(this.handleLogin.bind(this))
          .catch(err =>
            Promise.resolve()
              .then(this.emit.bind(this, 'loginError', err))
              .then(() => Promise.reject(err)),
          );
  }
}

DDP.models = {};
DDP.UnknownModel = class UnknownModel {
  constructor(doc) {
    Object.assign(this, doc);
  }
};

export default DDP;
<commit_msg>Allow specifying custom login promise<commit_after>import {
  EventEmitter,
  Queue,
} from '@theclinician/toolbelt';
import Socket from './socket';
import Method from './Method.js';
import Subscription from './Subscription.js';
import {
  once,
  omit,
  uniqueId,
} from './utils';
import {
  DDPError,
} from './errors.js';
import * as multiStorage from './multiStorage.js';

const DDP_VERSION = '1';
const DEFAULT_RECONNECT_INTERVAL = 10000;

interface AsyncStorage {
  get(string): Promise;
  set(string, mixed): Promise;
  del(string): Promise;
}

class DDP extends EventEmitter {
  status: string;
  subscriptions: { [string]: Subscription };
  methods: { [string]: Method };
  socket: Socket;
  methodsQueue: Queue;
  collections: { [string]: { [string]: mixed } };
  models: { [string]: Function };
  storage: AsyncStorage;

  static registerModel(Model, collection) {
    if (!collection) {
      throw Error('Method registerModel() requires collection name as the second argument');
    }
    if (this.models[collection]) {
      console.error(`Overwriting model ${this.models[collection].name} bound to collection ${collection}; new model is ${Model.name}`);
    }
    this.models[Model.collection] = Model;
  }

  constructor(options: {
    debug?: boolean,
    endpoint: string,
    autoConnect?: boolean,
    autoReconnect?: boolean,
    reconnectInterval?: number,
    storage?: AsyncStorage,
  }) {
    super();

    this.captureUnhandledErrors([
      'error',
      'loginError',
      'resumeLoginError',
      'logoutError',
    ]);

    this.status = 'disconnected';
    this.endpoint = options.endpoint;

    // Default `autoConnect` and `autoReconnect` to true
    this.autoConnect = (options.autoConnect !== false);
    this.autoReconnect = (options.autoReconnect !== false);
    this.reconnectInterval = options.reconnectInterval || DEFAULT_RECONNECT_INTERVAL;
    this.collections = {};
    this.storage = options.storage || multiStorage;

    // Methods/ subscriptions handlers
    this.subscriptions = {};
    this.methods = {};

    this.methodsQueue = new Queue({
      onError: () => false, // make sure the queue is not terminated even if one error occurs
    });
    this.methodsQueue.pause();

    // Socket
    this.socket = new Socket(options.SocketConstructor, options.endpoint);

    if (options.debug) {
      this.socket.on('message:out', message => console.warn('DDP/OUT', message));
      this.socket.on('message:in', message => console.warn('DDP/IN', message));
    }

    this.socket.on('open', () => {
      // When the socket opens, send the `connect` message
      // to establish the DDP connection
      this.socket.send({
        msg: 'connect',
        version: DDP_VERSION,
        support: [DDP_VERSION],
      });
    });

    this.socket.on('close', () => {
      this.status = 'disconnected';

      this.methodsQueue.pause();
      this.methodsQueue.clear();
      this.cancelPendingMethods(new Error('Connection was lost'));

      this.emit('disconnected');
      if (this.autoReconnect) {
        // Schedule a connection
        setTimeout(
          this.socket.open.bind(this.socket),
          this.reconnectInterval,
        );
      }
    });

    this.socket.on('message:in', (message) => {
      switch (message.msg) {
        case 'connected':
          this.status = 'connected';
          this.resumeLogin().then(() => {
            this.methodsQueue.resume();
            this.restoreSubscriptions();
            this.emit('connected');
          });
          break;
        case 'ping':
          // Reply with a `pong` message to prevent the server from
          // closing the connection
          this.socket.send({ msg: 'pong', id: message.id });
          break;
        case 'ready':
          message.subs.forEach(id => this.subscriptions[id] && this.subscriptions[id].ready());
          break;
        case 'nosub':
          if (this.subscriptions[message.id]) {
            this.subscriptions[message.id].nosub(message.error);
          }
          break;
        case 'result':
          if (this.methods[message.id]) {
            const { error, result } = message;
            this.methods[message.id].result({
              error,
              result,
            });
          }
          break;
        case 'updated':
          message.methods.forEach(id => this.methods[id] && this.methods[id].updated());
          break;
        case 'error':
          this.emit('error', new DDPError('DDPError', message.reason));
          break;
        case 'added':
        case 'changed':
        case 'removed':
          this[message.msg](message);
          break;
        default:
          // ignore the message ...
      }
    });

    if (this.autoConnect) {
      this.connect();
    }
  }

  connect() {
    this.socket.open();
  }

  added({ collection, id, fields }) {
    const Model = this.constructor.models[collection] || this.constructor.UnknownModel;
    if (Model) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: new Model({
            _id: id,
            ...fields,
          }),
        },
      };
      this.emit('dataUpdated', this.collections);
    }
    this.emit('added', { collection, id, fields });
  }

  changed({ collection, id, fields, cleared }) {
    const Model = this.constructor.models[collection] || this.constructor.UnknownModel;
    if (Model) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: new Model(omit({
            ...this.collections[collection][id],
            ...fields,
          }, cleared)),
        },
      };
      this.emit('dataUpdated', this.collections);
    }
    this.emit('changed', { collection, id, fields });
  }

  removed({ collection, id }) {
    const Model = this.constructor.models[collection] || this.constructor.UnknownModel;
    if (Model) {
      this.collections = {
        ...this.collections,
        [collection]: this.collections[collection]
          ? omit(this.collections[collection], [id])
          : {},
      };
      this.emit('dataUpdated', this.collections);
    }
    this.emit('removed', { collection, id });
  }

  disconnect() {
    /**
     * If `disconnect` is called, the caller likely doesn't want the
     * the instance to try to auto-reconnect. Therefore we set the
     * `autoReconnect` flag to false.
     */
    this.autoReconnect = false;
    this.socket.close();
  }

  cancelPendingMethods(err) {
    Object.keys(this.methods).forEach((id) => {
      this.methods[id].cancel(err);
    });
  }

  restoreSubscriptions() {
    let numberOfPending = Object.keys(this.subscriptions).length;

    const cb = () => {
      numberOfPending -= 1;
      if (numberOfPending === 0) {
        this.emit('restored');
      }
    };

    if (numberOfPending > 0 && this.status === 'connected') {
      this.emit('restoring');

      Object.keys(this.subscriptions).forEach((id) => {
        this.subscriptions[id].setCallback(cb);
        this.socket.send(this.subscriptions[id].toDDPMessage(id));
      });
    }
  }

  subscribe(name, params, { onStop, onReady } = {}) {
    const id = uniqueId();

    this.subscriptions[id] = new Subscription({
      name,
      params,
      onStop,
      onReady,
    });

    if (this.status === 'connected') {
      this.socket.send(this.subscriptions[id].toDDPMessage(id));
    }

    return {
      stop: once(() => {
        if (this.status === 'connected') {
          this.socket.send({ id, msg: 'unsub' });
        }
        delete this.subscriptions[id];
        if (onStop) {
          onStop();
        }
      }),
    };
  }

  apply(name, params, {
    wait,
    noRetry,
    skipQueue,
    onResultReceived,
    throwStubExceptions,
  } = {}, asyncCallback) {
    if (!asyncCallback) {
      return new Promise((resolve, reject) => {
        this.apply(name, params, {
          wait,
          noRetry,
          skipQueue,
          onResultReceived,
          throwStubExceptions,
        }, (error, result) => (error ? reject(error) : resolve(result)));
      });
    }

    const id = uniqueId();

    this.methods[id] = new Method({
      name,
      params,
      noRetry,
      onResultReceived,
      asyncCallback,
    });

    const action = (cb) => {
      const method = this.methods[id];
      if (method) {
        this.methods[id].setCallback((error, result) => {
          if (cb) {
            cb(error, result);
          }
          delete this.methods[id];
        });
        if (this.status === 'connected') {
          this.socket.send(method.toDDPMessage(id));
        } else {
          // NOTE: Theoretically this should not happen since we are pausing
          //       queue when status in disconnected. Better safe than sorry.
          this.methods[id].cancel();
        }
      }
    };

    if (skipQueue) {
      action(null);
    } else {
      this.methodsQueue.push({
        noWait: !wait,
        onStop: (err) => {
          // This can be called when queue is clear, e.g. when connection is lost.
          if (this.methods[id]) {
            this.methods[id].cancel(err);
          }
        },
        action,
      });
    }

    return undefined;
  }

  handleLogin({ id, token }) {
    this.userId = id;
    return this.storage
          .set(`${this.endpoint}__login_token__`, token)
          .then(this.emit.bind(this, 'loggedIn', id))
          .then(() => id);
  }

  handleLogout() {
    this.userId = null;
    return this.storage
          .del(`${this.endpoint}__login_token__`)
          .then(this.emit.bind(this, 'loggedOut'))
          .then(() => null);
  }

  resumeLogin() {
    // NOTE: This promise never rejects
    return Promise.resolve()
          .then(this.emit.bind(this, 'loggingIn'))
          .then(() => this.storage.get(`${this.endpoint}__login_token__`))
          .then((resume) => {
            if (!resume) {
              // NOTE: It's important to emit loginError here, because we've already
              //       triggered loggingIn. So by triggering error as well we are
              //       indicating that the login procedure was actually terminated.
              this.emit('loginError', new Error('No login token'));
              return undefined;
            }
            return this.login({ resume }, { skipQueue: true });
          })
          .catch(err =>
            Promise.resolve()
              .then(this.handleLogout.bind(this))
              .then(this.emit.bind(this, 'resumeLoginError', err)),
          );
  }

  login(options, { skipQueue } = {}) {
    return this.executeLoginRoutine('login', [options], { skipQueue });
  }

  logout() {
    return Promise.resolve()
          .then(this.emit.bind(this, 'loggingIn'))
          .then(this.apply.bind(this, 'logout', []))
          .then(this.handleLogout.bind(this))
          .catch(err =>
            Promise.resolve()
              .then(this.handleLogout.bind(this))
              .then(this.emit.bind(this, 'logoutError', err))
              .then(() => Promise.reject(err)),
          );
  }

  executeLoginRoutine(name, params, { skipQueue = false } = {}) {
    let getLoginPromise;
    if (typeof name === 'function') {
      getLoginPromise = name;
    } else {
      getLoginPromise = () => this.apply(name, params, { skipQueue, wait: true });
    }
    return Promise.resolve()
          .then(this.emit.bind(this, 'loggingIn'))
          .then(() => getLoginPromise())
          .then(this.handleLogin.bind(this))
          .catch(err =>
            Promise.resolve()
              .then(this.emit.bind(this, 'loginError', err))
              .then(() => Promise.reject(err)),
          );
  }
}

DDP.models = {};
DDP.UnknownModel = class UnknownModel {
  constructor(doc) {
    Object.assign(this, doc);
  }
};

export default DDP;
<|endoftext|>


<commit_before>//  Flocking system
//  Based on chapter 6 of Nature of Code by Daniel Shiffman
//  http://natureofcode.com/book/chapter-6-autonomous-agents/
//
//  Rewritten for PaperJS and optimized by Bob Corporaal - https://reefscape.net

//
//  Ideas for features and optimization

//  IDEA 1
//  Preprocess all the boids and store the distances in a matrix
//  -> this reduces the number of calculations
//  -> give each boid an id to easily find itself in the matrix

//  IDEA 2
//  do all calculations in one loop, reduce duplication

//  IDEA 3
//  use a quadtree or similar to find the nearest neighbour

//  IDEA 4 - DONE
//  let the boids react to the mouse cursor

//  IDEA 5
//  give the boids a limited field of view

//  IDEA 6
//  show the different vectors on one of the boids

//  IDEA 7
//  add wander behavior if boid is not close to anybody else

var Boid = Base.extend({
  initialize: function(x, y) {
    this.position = new Point(x, y);
    this.acceleration = new Point(0, 0);
    this.velocity = new Point(Math.random() * 2 - 1, Math.random() * 2 - 1);

    this.r = 3.0; // wraparound distance
    this.maxspeed = 3; // Maximum speed - original 3
    this.maxforce = 0.05; // Maximum steering force - original 0.05
    this.desiredseparation = 25.0; // Desired separation between boids - original 25.0
    this.alignmentneighbordist = 100; // Distance to follow average velocity - original 50
    this.cohesionneighbordist = 100; // Distance to steer to 'center of gravity' - original 50
    this.separationweight = 1.5; // weight of the separation vector
    this.alignmentweight = 1.0; // weight of the alignment vector
    this.cohesionweight = 1.0; // weight of the cohesion vector
    this.avoidweight = 0.2; // weight of the avoid vector
    this.avoidDistance = 100; // distance to stay away from the mouse

    //
    //  draw base boid arrow
    //
    var arrowLength = 8;
    var arrowSideLength = 8;

    var arrowSide = new Point(arrowSideLength, 0);
    var arrowStart = new Point(0, 0);
    var arrowEnd = arrowStart.subtract(new Point(arrowLength, 0));

    this.arrow = new Group([
      new Path([arrowEnd, arrowStart]),
      new Path([
        arrowStart.add(arrowSide.rotate(135)),
        arrowStart,
        arrowStart.add(arrowSide.rotate(-135))
      ])
    ]);

    this.arrow.strokeWidth = 1;
    this.arrow.strokeColor = '#1C1A20';
    this.arrow.applyMatrix = false;
  },

  run: function(boids, currentMousePos) {
    this.flock(boids, currentMousePos);
    this.update();
    this.borders();
    this.render();
  },

  applyForce: function(force) {
    // We could add mass here if we want A = F / M
    this.acceleration = this.acceleration.add(force);
  },

  flock: function(boids, currentMousePos) {

    var sep = this.separate(boids); // Separation
    var ali = this.alignment(boids); // Alignment
    var coh = this.cohesion(boids); // Cohesion
    var avo = this.avoid(currentMousePos); // Avoid

    //  Arbitrarily weight these forces
    //  OPTIMIZE: do this multiplication together with other multiplications on this vector
    sep = sep.multiply(this.separationweight);
    ali = ali.multiply(this.alignmentweight);
    coh = coh.multiply(this.cohesionweight);
    avo = avo.multiply(this.avoidweight);

    // Add the force vectors to acceleration
    // OPTIMIZE: there is no need for a separate function for this
    this.applyForce(sep);
    this.applyForce(ali);
    this.applyForce(coh);
    this.applyForce(avo);
  },

  update: function() {

    // Update velocity
    this.velocity = this.velocity.add(this.acceleration);

    // Limit speed
    if (this.velocity.length > this.maxspeed) {
      this.velocity = this.velocity.normalize(this.maxspeed);
    }

    this.position = this.position.add(this.velocity);

    // Reset acceleration to 0 each cycle
    this.acceleration = new Point(0, 0);
  },

  seek: function(target) {
    // A vector pointing from the location to the target
    var desired = target.subtract(this.position);

    // Normalize desired and scale to maximum speed
    desired = desired.normalize(this.maxspeed);

    var steer = desired.subtract(this.velocity);

    if (steer.length > this.maxforce) {
      steer = steer.normalize(this.maxforce);
    }

    return steer;
  },

  borders: function() {
    var width = view.viewSize.width;
    var height = view.viewSize.height;
    if (this.position.x < -this.r) this.position.x = width + this.r;
    if (this.position.y < -this.r) this.position.y = height + this.r;
    if (this.position.x > (width + this.r)) this.position.x = -this.r;
    if (this.position.y > (height + this.r)) this.position.y = -this.r;
  },

  render: function() {
    this.arrow.position = this.position;
    this.arrow.rotate(this.velocity.angle - this.arrow.rotation);
  },

  // Separation
  // Method checks for nearby boids and steers away
  separate: function(boids) {

    var steer = new Point(0, 0);
    var count = 0;
    // For every boid in the system, check if it's too close
    var n = boids.length;
    for (var i = 0; i < n; i++) {

      var d = this.position.getDistance(boids[i].position);
      // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < this.desiredseparation)) {
        // Calculate vector pointing away from neighbor
        var diff = this.position.subtract(boids[i].position);
        diff = diff.normalize();
        diff = diff.divide(d); // Weight by distance
        steer = steer.add(diff);
        count++; // Keep track of how many are too close
      }
    }
    // Average -- divide by how many
    if (count > 0) {
      steer = steer.divide(count);
    }

    // As long as the vector is greater than 0
    if (steer.length > 0) {
      // Implement Reynolds: Steering = Desired - Velocity
      steer = steer.normalize();
      steer = steer.multiply(this.maxspeed);
      steer = steer.subtract(this.velocity);
      if (steer.length > this.maxforce) {
        steer = steer.normalize(this.maxforce);
      }
    }
    return steer;
  },

  // Alignment
  // For every nearby boid in the system, calculate the average velocity
  alignment: function(boids) {

    var sum = new Point(0, 0);
    var count = 0;
    var n = boids.length;
    for (var i = 0; i < boids.length; i++) {
      // OPTIMIZE: no need to calculate this twice!
      var d = this.position.getDistance(boids[i].position);
      if ((d > 0) && (d < this.alignmentneighbordist)) {
        sum = sum.add(boids[i].velocity);
        count++;
      }
    }

    if (count > 0) {
      // OPTIMIZE: this code is repeated in other places
      sum = sum.divide(count);
      sum = sum.normalize(this.maxspeed);
      var steer = sum.subtract(this.velocity);
      if (steer.length > this.maxforce) {
        steer = steer.normalize(this.maxforce);
      }
      return steer;
    } else {
      return new Point(0, 0); // OPTIMIZE: If count == 0 then steer should also be 0,0 so always send steer
    }
  },

  // Cohesion
  // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
  cohesion: function(boids) {

    var sum = new Point(0, 0); // Start with empty vector to accumulate all locations
    var count = 0;
    var n = boids.length;

    for (var i = 0; i < n; i++) {
      var d = this.position.getDistance(boids[i].position);
      if ((d > 0) && (d < this.cohesionneighbordist)) {
        sum = sum.add(boids[i].position); // Add location
        count++;
      }
    }

    if (count > 0) {
      sum = sum.divide(count);
      return this.seek(sum); // Steer towards the location
    } else {
      return new Point();
    }

  },

  //  Avoid
  //  Stay away from the current mouse position
  avoid: function(currentMousePos) {
    var ap = currentMousePos.subtract(this.position);

    if (ap.length < this.avoidDistance) {
      var ab = this.position.add(this.velocity);

      ab = ab.normalize();
      ab = ab.multiply(ap.dot(ab));

      var avoidVector = ab.subtract(ap);

      return avoidVector.normalize();
    } else {
      return new Point(0, 0);
    }
  }
});
<commit_msg>use local variable for loop<commit_after>//  Flocking system
//  Based on chapter 6 of Nature of Code by Daniel Shiffman
//  http://natureofcode.com/book/chapter-6-autonomous-agents/
//
//  Rewritten for PaperJS and optimized by Bob Corporaal - https://reefscape.net

//
//  Ideas for features and optimization

//  IDEA 1
//  Preprocess all the boids and store the distances in a matrix
//  -> this reduces the number of calculations
//  -> give each boid an id to easily find itself in the matrix

//  IDEA 2
//  do all calculations in one loop, reduce duplication

//  IDEA 3
//  use a quadtree or similar to find the nearest neighbour

//  IDEA 4 - DONE
//  let the boids react to the mouse cursor

//  IDEA 5
//  give the boids a limited field of view

//  IDEA 6
//  show the different vectors on one of the boids

//  IDEA 7
//  add wander behavior if boid is not close to anybody else

var Boid = Base.extend({
  initialize: function(x, y) {
    this.position = new Point(x, y);
    this.acceleration = new Point(0, 0);
    this.velocity = new Point(Math.random() * 2 - 1, Math.random() * 2 - 1);

    this.r = 3.0; // wraparound distance
    this.maxspeed = 3; // Maximum speed - original 3
    this.maxforce = 0.05; // Maximum steering force - original 0.05
    this.desiredseparation = 25.0; // Desired separation between boids - original 25.0
    this.alignmentneighbordist = 100; // Distance to follow average velocity - original 50
    this.cohesionneighbordist = 100; // Distance to steer to 'center of gravity' - original 50
    this.separationweight = 1.5; // weight of the separation vector
    this.alignmentweight = 1.0; // weight of the alignment vector
    this.cohesionweight = 1.0; // weight of the cohesion vector
    this.avoidweight = 0.2; // weight of the avoid vector
    this.avoidDistance = 100; // distance to stay away from the mouse

    //
    //  draw base boid arrow
    //
    var arrowLength = 8;
    var arrowSideLength = 8;

    var arrowSide = new Point(arrowSideLength, 0);
    var arrowStart = new Point(0, 0);
    var arrowEnd = arrowStart.subtract(new Point(arrowLength, 0));

    this.arrow = new Group([
      new Path([arrowEnd, arrowStart]),
      new Path([
        arrowStart.add(arrowSide.rotate(135)),
        arrowStart,
        arrowStart.add(arrowSide.rotate(-135))
      ])
    ]);

    this.arrow.strokeWidth = 1;
    this.arrow.strokeColor = '#1C1A20';
    this.arrow.applyMatrix = false;
  },

  run: function(boids, currentMousePos) {
    this.flock(boids, currentMousePos);
    this.update();
    this.borders();
    this.render();
  },

  applyForce: function(force) {
    // We could add mass here if we want A = F / M
    this.acceleration = this.acceleration.add(force);
  },

  flock: function(boids, currentMousePos) {

    var sep = this.separate(boids); // Separation
    var ali = this.alignment(boids); // Alignment
    var coh = this.cohesion(boids); // Cohesion
    var avo = this.avoid(currentMousePos); // Avoid

    //  Arbitrarily weight these forces
    //  OPTIMIZE: do this multiplication together with other multiplications on this vector
    sep = sep.multiply(this.separationweight);
    ali = ali.multiply(this.alignmentweight);
    coh = coh.multiply(this.cohesionweight);
    avo = avo.multiply(this.avoidweight);

    // Add the force vectors to acceleration
    // OPTIMIZE: there is no need for a separate function for this
    this.applyForce(sep);
    this.applyForce(ali);
    this.applyForce(coh);
    this.applyForce(avo);
  },

  update: function() {

    // Update velocity
    this.velocity = this.velocity.add(this.acceleration);

    // Limit speed
    if (this.velocity.length > this.maxspeed) {
      this.velocity = this.velocity.normalize(this.maxspeed);
    }

    this.position = this.position.add(this.velocity);

    // Reset acceleration to 0 each cycle
    this.acceleration = new Point(0, 0);
  },

  seek: function(target) {
    // A vector pointing from the location to the target
    var desired = target.subtract(this.position);

    // Normalize desired and scale to maximum speed
    desired = desired.normalize(this.maxspeed);

    var steer = desired.subtract(this.velocity);

    if (steer.length > this.maxforce) {
      steer = steer.normalize(this.maxforce);
    }

    return steer;
  },

  borders: function() {
    var width = view.viewSize.width;
    var height = view.viewSize.height;
    if (this.position.x < -this.r) this.position.x = width + this.r;
    if (this.position.y < -this.r) this.position.y = height + this.r;
    if (this.position.x > (width + this.r)) this.position.x = -this.r;
    if (this.position.y > (height + this.r)) this.position.y = -this.r;
  },

  render: function() {
    this.arrow.position = this.position;
    this.arrow.rotate(this.velocity.angle - this.arrow.rotation);
  },

  // Separation
  // Method checks for nearby boids and steers away
  separate: function(boids) {

    var steer = new Point(0, 0);
    var count = 0;
    // For every boid in the system, check if it's too close
    var n = boids.length;
    for (var i = 0; i < n; i++) {

      var d = this.position.getDistance(boids[i].position);
      // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < this.desiredseparation)) {
        // Calculate vector pointing away from neighbor
        var diff = this.position.subtract(boids[i].position);
        diff = diff.normalize();
        diff = diff.divide(d); // Weight by distance
        steer = steer.add(diff);
        count++; // Keep track of how many are too close
      }
    }
    // Average -- divide by how many
    if (count > 0) {
      steer = steer.divide(count);
    }

    // As long as the vector is greater than 0
    if (steer.length > 0) {
      // Implement Reynolds: Steering = Desired - Velocity
      steer = steer.normalize();
      steer = steer.multiply(this.maxspeed);
      steer = steer.subtract(this.velocity);
      if (steer.length > this.maxforce) {
        steer = steer.normalize(this.maxforce);
      }
    }
    return steer;
  },

  // Alignment
  // For every nearby boid in the system, calculate the average velocity
  alignment: function(boids) {

    var sum = new Point(0, 0);
    var count = 0;
    var n = boids.length;
    for (var i = 0; i < n; i++) {
      // OPTIMIZE: no need to calculate this twice!
      var d = this.position.getDistance(boids[i].position);
      if ((d > 0) && (d < this.alignmentneighbordist)) {
        sum = sum.add(boids[i].velocity);
        count++;
      }
    }

    if (count > 0) {
      // OPTIMIZE: this code is repeated in other places
      sum = sum.divide(count);
      sum = sum.normalize(this.maxspeed);
      var steer = sum.subtract(this.velocity);
      if (steer.length > this.maxforce) {
        steer = steer.normalize(this.maxforce);
      }
      return steer;
    } else {
      return new Point(0, 0); // OPTIMIZE: If count == 0 then steer should also be 0,0 so always send steer
    }
  },

  // Cohesion
  // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
  cohesion: function(boids) {

    var sum = new Point(0, 0); // Start with empty vector to accumulate all locations
    var count = 0;
    var n = boids.length;

    for (var i = 0; i < n; i++) {
      var d = this.position.getDistance(boids[i].position);
      if ((d > 0) && (d < this.cohesionneighbordist)) {
        sum = sum.add(boids[i].position); // Add location
        count++;
      }
    }

    if (count > 0) {
      sum = sum.divide(count);
      return this.seek(sum); // Steer towards the location
    } else {
      return new Point();
    }

  },

  //  Avoid
  //  Stay away from the current mouse position
  avoid: function(currentMousePos) {
    var ap = currentMousePos.subtract(this.position);

    if (ap.length < this.avoidDistance) {
      var ab = this.position.add(this.velocity);

      ab = ab.normalize();
      ab = ab.multiply(ap.dot(ab));

      var avoidVector = ab.subtract(ap);

      return avoidVector.normalize();
    } else {
      return new Point(0, 0);
    }
  }
});
<|endoftext|>


<commit_before>/**
 * Braintree Google Pay button
 **/
define(
    [
        'uiComponent',
        "knockout",
        "jquery",
        'braintree',
        'braintreeGooglePay',
        'mage/translate',
        'googlePayLibrary'
        ],
    function (
        Component,
        ko,
        jQuery,
        braintree,
        googlePay,
        $t
    ) {
        'use strict';

        return {
            init: function (element, context) {
                // No element or context
                if (!element || !context || !this.deviceSupported()) {
                    return;
                }

                // Context must implement these methods
                if (typeof context.getClientToken !== 'function') {
                    console.error("Braintree GooglePay Context passed does not provide a getClientToken method", context);
                    return;
                }
                if (typeof context.getPaymentRequest !== 'function') {
                    console.error("Braintree GooglePay Context passed does not provide a getPaymentRequest method", context);
                    return;
                }
                if (typeof context.startPlaceOrder !== 'function') {
                    console.error("Braintree GooglePay Context passed does not provide a startPlaceOrder method", context);
                    return;
                }

                // init google pay object
                var paymentsClient = new google.payments.api.PaymentsClient({
                    environment: context.getEnvironment()
                });

                // Create a button within the KO element, as google pay can only be instantiated through
                // a valid on click event (ko onclick bind interferes with this).
                var button = document.createElement('button');
                button.className = "braintree-googlepay-button long " + (context.getBtnColor() == 1 ? 'black' : 'white');
                button.title = $t("Buy with Google Pay");

                // init braintree api
                braintree.create({
                    authorization: context.getClientToken()
                }, function (clientErr, clientInstance) {
                    if (clientErr) {
                        console.error('Error creating client:', clientErr);
                        return;
                    }

                    googlePay.create({
                        client: clientInstance
                    }, function (googlePayErr, googlePaymentInstance) {
                        // No instance
                        if (googlePayErr) {
                            console.error('Braintree GooglePay Error creating googlePayInstance:', googlePayErr);
                            return;
                        }

                        paymentsClient.isReadyToPay({
                            allowedPaymentMethods: googlePaymentInstance.createPaymentDataRequest().allowedPaymentMethods
                        }).then(function(response) {
                            if (response.result) {
                                button.addEventListener('click', function (event) {
                                    event.preventDefault();
                                    jQuery("body").loader('show');
                                    var responseData;

                                    var paymentDataRequest = googlePaymentInstance.createPaymentDataRequest(context.getPaymentRequest());
                                    paymentsClient.loadPaymentData(paymentDataRequest).then(function (paymentData) {
                                        // Persist the paymentData (shipping address etc)
                                        responseData = paymentData;
                                        // Return the braintree nonce promise
                                        return googlePaymentInstance.parseResponse(paymentData);
                                    }).then(function (result) {
                                        context.startPlaceOrder(result.nonce, responseData);
                                    }).catch(function (err) {
                                        // Handle errors
                                        // err = {statusCode: "CANCELED"}
                                        console.error(err);
                                        jQuery("body").loader('hide');
                                    });
                                });

                                element.appendChild(button);
                            }
                        }).catch(function (err) {
                            console.error(err);
                            jQuery("body").loader('hide');
                        });
                    });
                });
            },

            deviceSupported: function() {
                return !!(window.PaymentRequest);
            }
        };
    }
);
<commit_msg>#78181 - Add validation<commit_after>/**
 * Braintree Google Pay button
 **/
 define(
    [
        'uiComponent',
        "knockout",
        "jquery",
        'braintree',
        'braintreeGooglePay',
        'mage/translate',
        'Magento_Checkout/js/model/payment/additional-validators',
        'googlePayLibrary'
    ],
    function (
        Component,
        ko,
        jQuery,
        braintree,
        googlePay,
        $t,
        additionalValidators
    ) {
        'use strict';

        return {
            init: function (element, context) {
                // No element or context
                if (!element || !context || !this.deviceSupported()) {
                    return;
                }

                // Context must implement these methods
                if (typeof context.getClientToken !== 'function') {
                    console.error("Braintree GooglePay Context passed does not provide a getClientToken method", context);
                    return;
                }
                if (typeof context.getPaymentRequest !== 'function') {
                    console.error("Braintree GooglePay Context passed does not provide a getPaymentRequest method", context);
                    return;
                }
                if (typeof context.startPlaceOrder !== 'function') {
                    console.error("Braintree GooglePay Context passed does not provide a startPlaceOrder method", context);
                    return;
                }

                // init google pay object
                var paymentsClient = new google.payments.api.PaymentsClient({
                    environment: context.getEnvironment()
                });

                // Create a button within the KO element, as google pay can only be instantiated through
                // a valid on click event (ko onclick bind interferes with this).
                var button = document.createElement('button');
                button.className = "braintree-googlepay-button long " + (context.getBtnColor() == 1 ? 'black' : 'white');
                button.title = $t("Buy with Google Pay");

                // init braintree api
                braintree.create({
                    authorization: context.getClientToken()
                }, function (clientErr, clientInstance) {
                    if (clientErr) {
                        console.error('Error creating client:', clientErr);
                        return;
                    }

                    googlePay.create({
                        client: clientInstance
                    }, function (googlePayErr, googlePaymentInstance) {
                        // No instance
                        if (googlePayErr) {
                            console.error('Braintree GooglePay Error creating googlePayInstance:', googlePayErr);
                            return;
                        }

                        paymentsClient.isReadyToPay({
                            allowedPaymentMethods: googlePaymentInstance.createPaymentDataRequest().allowedPaymentMethods
                        }).then(function(response) {
                            if (response.result) {
                                button.addEventListener('click', function (event) {
                                    event.preventDefault();

                                    if (!additionalValidators.validate()) {
                                        return
                                    }

                                    jQuery("body").loader('show');
                                    var responseData;

                                    var paymentDataRequest = googlePaymentInstance.createPaymentDataRequest(context.getPaymentRequest());
                                    paymentsClient.loadPaymentData(paymentDataRequest).then(function (paymentData) {
                                        // Persist the paymentData (shipping address etc)
                                        responseData = paymentData;
                                        // Return the braintree nonce promise
                                        return googlePaymentInstance.parseResponse(paymentData);
                                    }).then(function (result) {
                                        context.startPlaceOrder(result.nonce, responseData);
                                    }).catch(function (err) {
                                        // Handle errors
                                        // err = {statusCode: "CANCELED"}
                                        console.error(err);
                                        jQuery("body").loader('hide');
                                    });
                                });

                                element.appendChild(button);
                            }
                        }).catch(function (err) {
                            console.error(err);
                            jQuery("body").loader('hide');
                        });
                    });
                });
            },

            deviceSupported: function() {
                return !!(window.PaymentRequest);
            }
        };
    }
);
<|endoftext|>


<commit_before>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('options.contentSettings', function() {
  const List = cr.ui.List;
  const ListItem = cr.ui.ListItem;
  const ArrayDataModel = cr.ui.ArrayDataModel;

  /**
   * Creates a new exceptions list item.
   * @param {string} contentType The type of the list.
   * @param {string} mode The browser mode, 'otr' or 'normal'.
   * @param {boolean} enableAskOption Whether to show an 'ask every time'
   *     option in the select.
   * @param {Object} exception A dictionary that contains the data of the
   *     exception.
   * @constructor
   * @extends {cr.ui.ListItem}
   */
  function ExceptionsListItem(contentType, mode, enableAskOption, exception) {
    var el = cr.doc.createElement('li');
    el.mode = mode;
    el.contentType = contentType;
    el.enableAskOption = enableAskOption;
    el.dataItem = exception;
    el.__proto__ = ExceptionsListItem.prototype;
    el.decorate();

    return el;
  }

  ExceptionsListItem.prototype = {
    __proto__: ListItem.prototype,

    /**
     * Called when an element is decorated as a list item.
     */
    decorate: function() {
      ListItem.prototype.decorate.call(this);

      // Labels for display mode.
      var patternLabel = cr.doc.createElement('span');
      patternLabel.textContent = this.pattern;
      this.appendChild(patternLabel);

      var settingLabel = cr.doc.createElement('span');
      settingLabel.textContent = this.settingForDisplay();
      settingLabel.className = 'exceptionSetting';
      this.appendChild(settingLabel);

      // Elements for edit mode.
      var input = cr.doc.createElement('input');
      input.type = 'text';
      this.appendChild(input);
      input.className = 'exceptionInput hidden';

      var select = cr.doc.createElement('select');
      var optionAllow = cr.doc.createElement('option');
      optionAllow.textContent = templateData.allowException;
      select.appendChild(optionAllow);

      if (this.enableAskOption) {
        var optionAsk = cr.doc.createElement('option');
        optionAsk.textContent = templateData.askException;
        select.appendChild(optionAsk);
        this.optionAsk = optionAsk;
      }

      if (this.contentType == 'cookies') {
        var optionSession = cr.doc.createElement('option');
        optionSession.textContent = templateData.sessionException;
        select.appendChild(optionSession);
        this.optionSession = optionSession;
      }

      var optionBlock = cr.doc.createElement('option');
      optionBlock.textContent = templateData.blockException;
      select.appendChild(optionBlock);

      this.appendChild(select);
      select.className = 'exceptionSetting hidden';

      // Used to track whether the URL pattern in the input is valid.
      // This will be true if the browser process has informed us that the
      // current text in the input is valid. Changing the text resets this to
      // false, and getting a response from the browser sets it back to true.
      // It starts off as false for empty string (new exceptions) or true for
      // already-existing exceptions (which we assume are valid).
      this.inputValidityKnown = this.pattern;
      // This one tracks the actual validity of the pattern in the input. This
      // starts off as true so as not to annoy the user when he adds a new and
      // empty input.
      this.inputIsValid = true;

      this.patternLabel = patternLabel;
      this.settingLabel = settingLabel;
      this.input = input;
      this.select = select;
      this.optionAllow = optionAllow;
      this.optionBlock = optionBlock;

      this.updateEditables();
      if (!this.pattern)
        input.value = templateData.examplePattern;

      var listItem = this;
      this.ondblclick = function(event) {
        // Editing notifications and geolocation is disabled for now.
        if (listItem.contentType == 'notifications' ||
            listItem.contentType == 'location')
          return;

        listItem.editing = true;
      };

      // Handle events on the editable nodes.
      input.oninput = function(event) {
        listItem.inputValidityKnown = false;
        chrome.send('checkExceptionPatternValidity',
                    [listItem.contentType, listItem.mode, input.value]);
      };

      // Handles enter and escape which trigger reset and commit respectively.
      function handleKeydown(e) {
        // Make sure that the tree does not handle the key.
        e.stopPropagation();

        // Calling list.focus blurs the input which will stop editing the list
        // item.
        switch (e.keyIdentifier) {
          case 'U+001B':  // Esc
            // Reset the inputs.
            listItem.updateEditables();
            if (listItem.pattern)
              listItem.maybeSetPatternValid(listItem.pattern, true);
          case 'Enter':
            if (listItem.parentNode)
              listItem.parentNode.focus();
        }
      }

      function handleBlur(e) {
        // When the blur event happens we do not know who is getting focus so we
        // delay this a bit since we want to know if the other input got focus
        // before deciding if we should exit edit mode.
        var doc = e.target.ownerDocument;
        window.setTimeout(function() {
          var activeElement = doc.activeElement;
          if (!listItem.contains(activeElement)) {
            listItem.editing = false;
          }
        }, 50);
      }

      input.addEventListener('keydown', handleKeydown);
      input.addEventListener('blur', handleBlur);

      select.addEventListener('keydown', handleKeydown);
      select.addEventListener('blur', handleBlur);
    },

    /**
     * The pattern (e.g., a URL) for the exception.
     * @type {string}
     */
    get pattern() {
      return this.dataItem['displayPattern'];
    },
    set pattern(pattern) {
      this.dataItem['displayPattern'] = pattern;
    },

    /**
     * The setting (allow/block) for the exception.
     * @type {string}
     */
    get setting() {
      return this.dataItem['setting'];
    },
    set setting(setting) {
      this.dataItem['setting'] = setting;
    },

    /**
     * Gets a human-readable setting string.
     * @type {string}
     */
    settingForDisplay: function() {
      var setting = this.setting;
      if (setting == 'allow')
        return templateData.allowException;
      else if (setting == 'block')
        return templateData.blockException;
      else if (setting == 'ask')
        return templateData.askException;
      else if (setting == 'session')
        return templateData.sessionException;
    },

    /**
     * Update this list item to reflect whether the input is a valid pattern
     * if |pattern| matches the text currently in the input.
     * @param {string} pattern The pattern.
     * @param {boolean} valid Whether said pattern is valid in the context of
     *     a content exception setting.
     */
    maybeSetPatternValid: function(pattern, valid) {
      // Don't do anything for messages where we are not the intended recipient,
      // or if the response is stale (i.e. the input value has changed since we
      // sent the request to analyze it).
      if (pattern != this.input.value)
        return;

      if (valid)
        this.input.setCustomValidity('');
      else
        this.input.setCustomValidity(' ');
      this.inputIsValid = valid;
      this.inputValidityKnown = true;
    },

    /**
     * Copy the data model values to the editable nodes.
     */
    updateEditables: function() {
      this.input.value = this.pattern;

      if (this.setting == 'allow')
        this.optionAllow.selected = true;
      else if (this.setting == 'block')
        this.optionBlock.selected = true;
      else if (this.setting == 'session' && this.optionSession)
        this.optionSession.selected = true;
      else if (this.setting == 'ask' && this.optionAsk)
        this.optionAsk.selected = true;
    },

    /**
     * Whether the user is currently able to edit the list item.
     * @type {boolean}
     */
    get editing() {
      return this.hasAttribute('editing');
    },
    set editing(editing) {
      var oldEditing = this.editing;
      if (oldEditing == editing)
        return;

      var listItem = this;
      var pattern = this.pattern;
      var setting = this.setting;
      var patternLabel = this.patternLabel;
      var settingLabel = this.settingLabel;
      var input = this.input;
      var select = this.select;
      var optionAllow = this.optionAllow;
      var optionBlock = this.optionBlock;
      var optionSession = this.optionSession;
      var optionAsk = this.optionAsk;

      // Just delete this row if it was added via the Add button.
      if (!editing && !pattern && !input.value) {
        var model = listItem.parentNode.dataModel;
        model.splice(model.indexOf(listItem.dataItem), 1);
        return;
      }

      // Check that we have a valid pattern and if not we do not change the
      // editing mode.
      if (!editing && (!this.inputValidityKnown || !this.inputIsValid)) {
        input.focus();
        input.select();
        return;
      }

      patternLabel.classList.toggle('hidden');
      settingLabel.classList.toggle('hidden');
      input.classList.toggle('hidden');
      select.classList.toggle('hidden');

      var doc = this.ownerDocument;
      if (editing) {
        this.setAttribute('editing', '');
        cr.ui.limitInputWidth(input, this, 20);
        input.focus();
        input.select();
      } else {
        this.removeAttribute('editing');

        var newPattern = input.value;

        var newSetting;
        if (optionAllow.selected)
          newSetting = 'allow';
        else if (optionBlock.selected)
          newSetting = 'block';
        else if (optionSession && optionSession.selected)
          newSetting = 'session';
        else if (optionAsk && optionAsk.selected)
          newSetting = 'ask';

        // Empty edit - do nothing.
        if (pattern == newPattern && newSetting == this.setting)
          return;

        this.pattern = patternLabel.textContent = newPattern;
        this.setting = newSetting;
        settingLabel.textContent = this.settingForDisplay();

        if (pattern != this.pattern) {
          chrome.send('removeExceptions',
                      [this.contentType, this.mode, pattern]);
        }

        chrome.send('setException',
                    [this.contentType, this.mode, this.pattern, this.setting]);
      }
    }
  };

  /**
   * Creates a new exceptions list.
   * @constructor
   * @extends {cr.ui.List}
   */
  var ExceptionsList = cr.ui.define('list');

  ExceptionsList.prototype = {
    __proto__: List.prototype,

    /**
     * Called when an element is decorated as a list.
     */
    decorate: function() {
      List.prototype.decorate.call(this);

      this.dataModel = new ArrayDataModel([]);

      // Whether the exceptions in this list allow an 'Ask every time' option.
      this.enableAskOption = (this.contentType == 'plugins');
    },

    /**
     * Creates an item to go in the list.
     * @param {Object} entry The element from the data model for this row.
     */
    createItem: function(entry) {
      return new ExceptionsListItem(this.contentType,
                                    this.mode,
                                    this.enableAskOption,
                                    entry);
    },

    /**
     * Adds an exception to the js model.
     * @param {Object} entry A dictionary of values for the exception.
     */
    addException: function(entry) {
      this.dataModel.push(entry);

      // When an empty row is added, put it into editing mode.
      if (!entry['displayPattern'] && !entry['setting']) {
        var index = this.dataModel.length - 1;
        var sm = this.selectionModel;
        sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
        this.scrollIndexIntoView(index);
        var li = this.getListItemByIndex(index);
        li.editing = true;
      }
    },

    /**
     * The browser has finished checking a pattern for validity. Update the
     * list item to reflect this.
     * @param {string} pattern The pattern.
     * @param {bool} valid Whether said pattern is valid in the context of
     *     a content exception setting.
     */
    patternValidityCheckComplete: function(pattern, valid) {
      for (var i = 0; i < this.dataModel.length; i++) {
        var listItem = this.getListItemByIndex(i);
        if (listItem)
          listItem.maybeSetPatternValid(pattern, valid);
      }
    },

    /**
     * Removes all exceptions from the js model.
     */
    clear: function() {
      this.dataModel = new ArrayDataModel([]);
    },

    /**
     * Removes all selected rows from browser's model.
     */
    removeSelectedRows: function() {
      // The first member is the content type; the rest of the values describe
      // the patterns we are removing.
      var args = [this.contentType];
      var selectedItems = this.selectedItems;
      for (var i = 0; i < selectedItems.length; i++) {
        if (this.contentType == 'location') {
          args.push(selectedItems[i]['origin']);
          args.push(selectedItems[i]['embeddingOrigin']);
        } else if (this.contentType == 'notifications') {
          args.push(selectedItems[i]['origin']);
          args.push(selectedItems[i]['setting']);
        } else {
          args.push(this.mode);
          args.push(selectedItems[i]['displayPattern']);
        }
      }

      chrome.send('removeExceptions', args);
    },

    /**
     * Puts the selected row in editing mode.
     */
    editSelectedRow: function() {
      var li = this.getListItem(this.selectedItem);
      if (li)
        li.editing = true;
    }
  };

  var ExceptionsArea = cr.ui.define('div');

  ExceptionsArea.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      // TODO(estade): need some sort of visual indication when the list is
      // empty.
      this.exceptionsList = this.querySelector('list');
      this.exceptionsList.contentType = this.contentType;
      this.exceptionsList.mode = this.mode;

      ExceptionsList.decorate(this.exceptionsList);
      this.exceptionsList.selectionModel.addEventListener(
          'change', this.handleOnSelectionChange_.bind(this));

      var self = this;
      if (this.contentType != 'location' &&
          this.contentType != 'notifications') {
        var addRow = cr.doc.createElement('button');
        addRow.textContent = templateData.addExceptionRow;
        this.appendChild(addRow);

        addRow.onclick = function(event) {
          var emptyException = new Object;
          emptyException.displayPattern = '';
          emptyException.setting = '';
          self.exceptionsList.addException(emptyException);
        };

        var editRow = cr.doc.createElement('button');
        editRow.textContent = templateData.editExceptionRow;
        this.appendChild(editRow);
        this.editRow = editRow;

        editRow.onclick = function(event) {
          self.exceptionsList.editSelectedRow();
        };
      }

      var removeRow = cr.doc.createElement('button');
      removeRow.textContent = templateData.removeExceptionRow;
      this.appendChild(removeRow);
      this.removeRow = removeRow;

      removeRow.onclick = function(event) {
        self.exceptionsList.removeSelectedRows();
      };

      this.updateButtonSensitivity();

      this.otrProfileExists = false;
    },

    /**
     * The content type for this exceptions area, such as 'images'.
     * @type {string}
     */
    get contentType() {
      return this.getAttribute('contentType');
    },
    set contentType(type) {
      return this.setAttribute('contentType', type);
    },

    /**
     * The browser mode type for this exceptions area, 'otr' or 'normal'.
     * @type {string}
     */
    get mode() {
      return this.getAttribute('mode');
    },
    set mode(mode) {
      return this.setAttribute('mode', mode);
    },

    /**
     * Update the enabled/disabled state of the editing buttons based on which
     * rows are selected.
     */
    updateButtonSensitivity: function() {
      var selectionSize = this.exceptionsList.selectedItems.length;
      if (this.editRow)
        this.editRow.disabled = selectionSize != 1;
      this.removeRow.disabled = selectionSize == 0;
    },

    /**
     * Callback from the selection model.
     * @param {!cr.Event} ce Event with change info.
     * @private
     */
    handleOnSelectionChange_: function(ce) {
      this.updateButtonSensitivity();
   },
  };

  return {
    ExceptionsListItem: ExceptionsListItem,
    ExceptionsList: ExceptionsList,
    ExceptionsArea: ExceptionsArea
  };
});
<commit_msg>Tabbed options - disable add/edit buttons in content settings lists while editing an exception.<commit_after>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('options.contentSettings', function() {
  const List = cr.ui.List;
  const ListItem = cr.ui.ListItem;
  const ArrayDataModel = cr.ui.ArrayDataModel;

  /**
   * Creates a new exceptions list item.
   * @param {string} contentType The type of the list.
   * @param {string} mode The browser mode, 'otr' or 'normal'.
   * @param {boolean} enableAskOption Whether to show an 'ask every time'
   *     option in the select.
   * @param {Object} exception A dictionary that contains the data of the
   *     exception.
   * @constructor
   * @extends {cr.ui.ListItem}
   */
  function ExceptionsListItem(contentType, mode, enableAskOption, exception) {
    var el = cr.doc.createElement('li');
    el.mode = mode;
    el.contentType = contentType;
    el.enableAskOption = enableAskOption;
    el.dataItem = exception;
    el.__proto__ = ExceptionsListItem.prototype;
    el.decorate();

    return el;
  }

  ExceptionsListItem.prototype = {
    __proto__: ListItem.prototype,

    /**
     * Called when an element is decorated as a list item.
     */
    decorate: function() {
      ListItem.prototype.decorate.call(this);

      // Labels for display mode.
      var patternLabel = cr.doc.createElement('span');
      patternLabel.textContent = this.pattern;
      this.appendChild(patternLabel);

      var settingLabel = cr.doc.createElement('span');
      settingLabel.textContent = this.settingForDisplay();
      settingLabel.className = 'exceptionSetting';
      this.appendChild(settingLabel);

      // Elements for edit mode.
      var input = cr.doc.createElement('input');
      input.type = 'text';
      this.appendChild(input);
      input.className = 'exceptionInput hidden';

      var select = cr.doc.createElement('select');
      var optionAllow = cr.doc.createElement('option');
      optionAllow.textContent = templateData.allowException;
      select.appendChild(optionAllow);

      if (this.enableAskOption) {
        var optionAsk = cr.doc.createElement('option');
        optionAsk.textContent = templateData.askException;
        select.appendChild(optionAsk);
        this.optionAsk = optionAsk;
      }

      if (this.contentType == 'cookies') {
        var optionSession = cr.doc.createElement('option');
        optionSession.textContent = templateData.sessionException;
        select.appendChild(optionSession);
        this.optionSession = optionSession;
      }

      var optionBlock = cr.doc.createElement('option');
      optionBlock.textContent = templateData.blockException;
      select.appendChild(optionBlock);

      this.appendChild(select);
      select.className = 'exceptionSetting hidden';

      // Used to track whether the URL pattern in the input is valid.
      // This will be true if the browser process has informed us that the
      // current text in the input is valid. Changing the text resets this to
      // false, and getting a response from the browser sets it back to true.
      // It starts off as false for empty string (new exceptions) or true for
      // already-existing exceptions (which we assume are valid).
      this.inputValidityKnown = this.pattern;
      // This one tracks the actual validity of the pattern in the input. This
      // starts off as true so as not to annoy the user when he adds a new and
      // empty input.
      this.inputIsValid = true;

      this.patternLabel = patternLabel;
      this.settingLabel = settingLabel;
      this.input = input;
      this.select = select;
      this.optionAllow = optionAllow;
      this.optionBlock = optionBlock;

      this.updateEditables();
      if (!this.pattern)
        input.value = templateData.examplePattern;

      var listItem = this;
      this.ondblclick = function(event) {
        // Editing notifications and geolocation is disabled for now.
        if (listItem.contentType == 'notifications' ||
            listItem.contentType == 'location')
          return;

        listItem.editing = true;
      };

      // Handle events on the editable nodes.
      input.oninput = function(event) {
        listItem.inputValidityKnown = false;
        chrome.send('checkExceptionPatternValidity',
                    [listItem.contentType, listItem.mode, input.value]);
      };

      // Handles enter and escape which trigger reset and commit respectively.
      function handleKeydown(e) {
        // Make sure that the tree does not handle the key.
        e.stopPropagation();

        // Calling list.focus blurs the input which will stop editing the list
        // item.
        switch (e.keyIdentifier) {
          case 'U+001B':  // Esc
            // Reset the inputs.
            listItem.updateEditables();
            if (listItem.pattern)
              listItem.maybeSetPatternValid(listItem.pattern, true);
          case 'Enter':
            if (listItem.parentNode)
              listItem.parentNode.focus();
        }
      }

      function handleBlur(e) {
        // When the blur event happens we do not know who is getting focus so we
        // delay this a bit since we want to know if the other input got focus
        // before deciding if we should exit edit mode.
        var doc = e.target.ownerDocument;
        window.setTimeout(function() {
          var activeElement = doc.activeElement;
          if (!listItem.contains(activeElement)) {
            listItem.editing = false;
          }
        }, 50);
      }

      input.addEventListener('keydown', handleKeydown);
      input.addEventListener('blur', handleBlur);

      select.addEventListener('keydown', handleKeydown);
      select.addEventListener('blur', handleBlur);
    },

    /**
     * The pattern (e.g., a URL) for the exception.
     * @type {string}
     */
    get pattern() {
      return this.dataItem['displayPattern'];
    },
    set pattern(pattern) {
      this.dataItem['displayPattern'] = pattern;
    },

    /**
     * The setting (allow/block) for the exception.
     * @type {string}
     */
    get setting() {
      return this.dataItem['setting'];
    },
    set setting(setting) {
      this.dataItem['setting'] = setting;
    },

    /**
     * Gets a human-readable setting string.
     * @type {string}
     */
    settingForDisplay: function() {
      var setting = this.setting;
      if (setting == 'allow')
        return templateData.allowException;
      else if (setting == 'block')
        return templateData.blockException;
      else if (setting == 'ask')
        return templateData.askException;
      else if (setting == 'session')
        return templateData.sessionException;
    },

    /**
     * Update this list item to reflect whether the input is a valid pattern
     * if |pattern| matches the text currently in the input.
     * @param {string} pattern The pattern.
     * @param {boolean} valid Whether said pattern is valid in the context of
     *     a content exception setting.
     */
    maybeSetPatternValid: function(pattern, valid) {
      // Don't do anything for messages where we are not the intended recipient,
      // or if the response is stale (i.e. the input value has changed since we
      // sent the request to analyze it).
      if (pattern != this.input.value)
        return;

      if (valid)
        this.input.setCustomValidity('');
      else
        this.input.setCustomValidity(' ');
      this.inputIsValid = valid;
      this.inputValidityKnown = true;
    },

    /**
     * Copy the data model values to the editable nodes.
     */
    updateEditables: function() {
      this.input.value = this.pattern;

      if (this.setting == 'allow')
        this.optionAllow.selected = true;
      else if (this.setting == 'block')
        this.optionBlock.selected = true;
      else if (this.setting == 'session' && this.optionSession)
        this.optionSession.selected = true;
      else if (this.setting == 'ask' && this.optionAsk)
        this.optionAsk.selected = true;
    },

    /**
     * Whether the user is currently able to edit the list item.
     * @type {boolean}
     */
    get editing() {
      return this.hasAttribute('editing');
    },
    set editing(editing) {
      var oldEditing = this.editing;
      if (oldEditing == editing)
        return;

      var listItem = this;
      var pattern = this.pattern;
      var setting = this.setting;
      var patternLabel = this.patternLabel;
      var settingLabel = this.settingLabel;
      var input = this.input;
      var select = this.select;
      var optionAllow = this.optionAllow;
      var optionBlock = this.optionBlock;
      var optionSession = this.optionSession;
      var optionAsk = this.optionAsk;

      // Just delete this row if it was added via the Add button.
      if (!editing && !pattern && !input.value) {
        var model = listItem.parentNode.dataModel;
        model.splice(model.indexOf(listItem.dataItem), 1);
        return;
      }

      // Check that we have a valid pattern and if not we do not change the
      // editing mode.
      if (!editing && (!this.inputValidityKnown || !this.inputIsValid)) {
        input.focus();
        input.select();
        return;
      }

      patternLabel.classList.toggle('hidden');
      settingLabel.classList.toggle('hidden');
      input.classList.toggle('hidden');
      select.classList.toggle('hidden');

      var doc = this.ownerDocument;
      var area = doc.querySelector('div[contentType=' +
          listItem.contentType + '][mode=' + listItem.mode + ']');
      area.enableAddAndEditButtons(!editing);

      if (editing) {
        this.setAttribute('editing', '');
        cr.ui.limitInputWidth(input, this, 20);
        input.focus();
        input.select();
      } else {
        this.removeAttribute('editing');

        var newPattern = input.value;

        var newSetting;
        if (optionAllow.selected)
          newSetting = 'allow';
        else if (optionBlock.selected)
          newSetting = 'block';
        else if (optionSession && optionSession.selected)
          newSetting = 'session';
        else if (optionAsk && optionAsk.selected)
          newSetting = 'ask';

        // Empty edit - do nothing.
        if (pattern == newPattern && newSetting == this.setting)
          return;

        this.pattern = patternLabel.textContent = newPattern;
        this.setting = newSetting;
        settingLabel.textContent = this.settingForDisplay();

        if (pattern != this.pattern) {
          chrome.send('removeExceptions',
                      [this.contentType, this.mode, pattern]);
        }

        chrome.send('setException',
                    [this.contentType, this.mode, this.pattern, this.setting]);
      }
    }
  };

  /**
   * Creates a new exceptions list.
   * @constructor
   * @extends {cr.ui.List}
   */
  var ExceptionsList = cr.ui.define('list');

  ExceptionsList.prototype = {
    __proto__: List.prototype,

    /**
     * Called when an element is decorated as a list.
     */
    decorate: function() {
      List.prototype.decorate.call(this);

      this.dataModel = new ArrayDataModel([]);

      // Whether the exceptions in this list allow an 'Ask every time' option.
      this.enableAskOption = (this.contentType == 'plugins');
    },

    /**
     * Creates an item to go in the list.
     * @param {Object} entry The element from the data model for this row.
     */
    createItem: function(entry) {
      return new ExceptionsListItem(this.contentType,
                                    this.mode,
                                    this.enableAskOption,
                                    entry);
    },

    /**
     * Adds an exception to the js model.
     * @param {Object} entry A dictionary of values for the exception.
     */
    addException: function(entry) {
      this.dataModel.push(entry);

      // When an empty row is added, put it into editing mode.
      if (!entry['displayPattern'] && !entry['setting']) {
        var index = this.dataModel.length - 1;
        var sm = this.selectionModel;
        sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
        this.scrollIndexIntoView(index);
        var li = this.getListItemByIndex(index);
        li.editing = true;
      }
    },

    /**
     * The browser has finished checking a pattern for validity. Update the
     * list item to reflect this.
     * @param {string} pattern The pattern.
     * @param {bool} valid Whether said pattern is valid in the context of
     *     a content exception setting.
     */
    patternValidityCheckComplete: function(pattern, valid) {
      for (var i = 0; i < this.dataModel.length; i++) {
        var listItem = this.getListItemByIndex(i);
        if (listItem)
          listItem.maybeSetPatternValid(pattern, valid);
      }
    },

    /**
     * Removes all exceptions from the js model.
     */
    clear: function() {
      this.dataModel = new ArrayDataModel([]);
    },

    /**
     * Removes all selected rows from browser's model.
     */
    removeSelectedRows: function() {
      // The first member is the content type; the rest of the values describe
      // the patterns we are removing.
      var args = [this.contentType];
      var selectedItems = this.selectedItems;
      for (var i = 0; i < selectedItems.length; i++) {
        if (this.contentType == 'location') {
          args.push(selectedItems[i]['origin']);
          args.push(selectedItems[i]['embeddingOrigin']);
        } else if (this.contentType == 'notifications') {
          args.push(selectedItems[i]['origin']);
          args.push(selectedItems[i]['setting']);
        } else {
          args.push(this.mode);
          args.push(selectedItems[i]['displayPattern']);
        }
      }

      chrome.send('removeExceptions', args);
    },

    /**
     * Puts the selected row in editing mode.
     */
    editSelectedRow: function() {
      var li = this.getListItem(this.selectedItem);
      if (li)
        li.editing = true;
    }
  };

  var ExceptionsArea = cr.ui.define('div');

  ExceptionsArea.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      // TODO(estade): need some sort of visual indication when the list is
      // empty.
      this.exceptionsList = this.querySelector('list');
      this.exceptionsList.contentType = this.contentType;
      this.exceptionsList.mode = this.mode;

      ExceptionsList.decorate(this.exceptionsList);
      this.exceptionsList.selectionModel.addEventListener(
          'change', this.handleOnSelectionChange_.bind(this));

      var self = this;
      if (this.contentType != 'location' &&
          this.contentType != 'notifications') {
        var addRow = cr.doc.createElement('button');
        addRow.textContent = templateData.addExceptionRow;
        this.appendChild(addRow);

        addRow.onclick = function(event) {
          var emptyException = new Object;
          emptyException.displayPattern = '';
          emptyException.setting = '';
          self.exceptionsList.addException(emptyException);
        };
        this.addRow = addRow;

        var editRow = cr.doc.createElement('button');
        editRow.textContent = templateData.editExceptionRow;
        this.appendChild(editRow);
        this.editRow = editRow;

        editRow.onclick = function(event) {
          self.exceptionsList.editSelectedRow();
        };
      }

      var removeRow = cr.doc.createElement('button');
      removeRow.textContent = templateData.removeExceptionRow;
      this.appendChild(removeRow);
      this.removeRow = removeRow;

      removeRow.onclick = function(event) {
        self.exceptionsList.removeSelectedRows();
      };

      this.updateButtonSensitivity();

      this.otrProfileExists = false;
    },

    /**
     * The content type for this exceptions area, such as 'images'.
     * @type {string}
     */
    get contentType() {
      return this.getAttribute('contentType');
    },
    set contentType(type) {
      return this.setAttribute('contentType', type);
    },

    /**
     * The browser mode type for this exceptions area, 'otr' or 'normal'.
     * @type {string}
     */
    get mode() {
      return this.getAttribute('mode');
    },
    set mode(mode) {
      return this.setAttribute('mode', mode);
    },

    /**
     * Update the enabled/disabled state of the editing buttons based on which
     * rows are selected.
     */
    updateButtonSensitivity: function() {
      var selectionSize = this.exceptionsList.selectedItems.length;
      if (this.addRow)
        this.addRow.disabled = this.addAndEditButtonsDisabled;
      if (this.editRow) {
        this.editRow.disabled = selectionSize != 1 ||
            this.addAndEditButtonsDisabled;
      }
      this.removeRow.disabled = selectionSize == 0;
    },

    /**
     * Manually toggle the enabled/disabled state for the add and edit buttons.
     * They'll be disabled while another row is being edited.
     * @param {boolean}
     */
    enableAddAndEditButtons: function(enable) {
      this.addAndEditButtonsDisabled = !enable;
      this.updateButtonSensitivity();
    },

    /**
     * Callback from the selection model.
     * @param {!cr.Event} ce Event with change info.
     * @private
     */
    handleOnSelectionChange_: function(ce) {
      this.updateButtonSensitivity();
   },
  };

  return {
    ExceptionsListItem: ExceptionsListItem,
    ExceptionsList: ExceptionsList,
    ExceptionsArea: ExceptionsArea
  };
});
<|endoftext|>


<commit_before>/**
 * Mailvelope - secure email with OpenPGP encryption for Webmail
 * Copyright (C) 2015 Mailvelope GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';

var mvelo = mvelo || null;
var QRCode = QRCode || null;

(function() {
  // communication to background page
  var port;
  // shares ID with KeyBackup Frame
  var id;
  // type + id
  var name;
  var l10n;
  // qrcoe object
  var qrcode;

  function init() {
    var qs = jQuery.parseQuerystring();
    id = qs.id;
    name = 'backupCodeWindow-' + id;
    // open port to background page
    port = mvelo.extension.connect({name: name});
    port.onMessage.addListener(messageListener);

    var formattedDate = new Date();

    $('#currentDate').html(formattedDate.toLocaleDateString());

    mvelo.l10n.localizeHTML();
    mvelo.l10n.getMessages([], function(result) {
      l10n = result;
    });
    mvelo.util.showSecurityBackground(qs.embedded);
    port.postMessage({event: 'get-logo-image', sender: name});
  }

  function setLogoImage(image) {
    if (image) {
      $('.logo').attr('src', image);
    }
    port.postMessage({event: 'get-backup-code', sender: name});
  }

  function setBackupCode(backupCode) {
    var length = 5, splitCode = '';
    $('.recovery-sheet_code-digit').each(function(index) {
      splitCode = backupCode.slice(length * index, (length * index) + length);
      $(this).text(splitCode);
    });
    $('.recovery-sheet_code-container').addClass('secureBackground');

    qrcode = new QRCode(document.getElementById('qrcode'), {
      text: backupCode,
      width: 175,
      height: 175,
      colorDark : "#000000",
      colorLight : "#ffffff",
      correctLevel : QRCode.CorrectLevel.H
    });

    $('.recovery-sheet_print-button').on('click', function() {
      window.print();
    });

    window.setTimeout(window.print, 500);
    port.postMessage({event: 'backup-code-window-init', sender: name});
  }

  function messageListener(msg) {
    switch (msg.event) {
      case 'set-backup-code':
        setBackupCode(msg.backupCode);
        break;
      case 'set-logo-image':
        setLogoImage(msg.image);
        break;
      default:
        console.log('unknown event');
    }
  }

  $(document).ready(init);

}());
<commit_msg>[MAMVELOPE-262] remove auto print call<commit_after>/**
 * Mailvelope - secure email with OpenPGP encryption for Webmail
 * Copyright (C) 2015 Mailvelope GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';

var mvelo = mvelo || null;
var QRCode = QRCode || null;

(function() {
  // communication to background page
  var port;
  // shares ID with KeyBackup Frame
  var id;
  // type + id
  var name;
  var l10n;
  // qrcoe object
  var qrcode;

  function init() {
    var qs = jQuery.parseQuerystring();
    id = qs.id;
    name = 'backupCodeWindow-' + id;
    // open port to background page
    port = mvelo.extension.connect({name: name});
    port.onMessage.addListener(messageListener);

    var formattedDate = new Date();

    $('#currentDate').html(formattedDate.toLocaleDateString());

    mvelo.l10n.localizeHTML();
    mvelo.l10n.getMessages([], function(result) {
      l10n = result;
    });
    mvelo.util.showSecurityBackground(qs.embedded);
    port.postMessage({event: 'get-logo-image', sender: name});
  }

  function setLogoImage(image) {
    if (image) {
      $('.logo').attr('src', image);
    }
    port.postMessage({event: 'get-backup-code', sender: name});
  }

  function setBackupCode(backupCode) {
    var length = 5, splitCode = '';
    $('.recovery-sheet_code-digit').each(function(index) {
      splitCode = backupCode.slice(length * index, (length * index) + length);
      $(this).text(splitCode);
    });
    $('.recovery-sheet_code-container').addClass('secureBackground');

    qrcode = new QRCode(document.getElementById('qrcode'), {
      text: backupCode,
      width: 175,
      height: 175,
      colorDark : "#000000",
      colorLight : "#ffffff",
      correctLevel : QRCode.CorrectLevel.H
    });

    $('.recovery-sheet_print-button').on('click', function() {
      window.print();
    });

    port.postMessage({event: 'backup-code-window-init', sender: name});
  }

  function messageListener(msg) {
    switch (msg.event) {
      case 'set-backup-code':
        setBackupCode(msg.backupCode);
        break;
      case 'set-logo-image':
        setLogoImage(msg.image);
        break;
      default:
        console.log('unknown event');
    }
  }

  $(document).ready(init);

}());
<|endoftext|>


<commit_before>#!/usr/bin/env node

'use strict';

const fs           = require('fs');
const gulp         = require('gulp');
const util         = require('gulp-util');
const plumber      = require('gulp-plumber');
const stylus       = require('gulp-stylus');
const less         = require('gulp-less');
const scss         = require('gulp-sass');
const autoprefixer = require('gulp-autoprefixer');
const groupMq      = require('gulp-group-css-media-queries');
const runSequence  = require('run-sequence');
const notifier     = require('node-notifier');
const bs           = require('browser-sync').create();
const program      = require('commander');
const jsonfile     = require('jsonfile');
const inquirer     = require('inquirer');
const pkg          = require('./package.json');
const questions    = [
    {
        type: 'input',
        name: 'host',
        message: 'Host for proxy'
    },
    {
        type    : 'input',
        name    : 'port',
        message : 'Server port',
        default : () => {
            return '7200';
        },
        validate: (value) => {

            let check = value.match(/^\d+$/);

            if (check) {
                return true;
            }

            return 'Use only numbers';
        }
    },
    {
        type: 'list',
        name: 'tech',
        message: 'What CSS pre-processor do you need?',
        choices: ['Styl', 'Scss', 'Less'],
        filter: function (val) {
            return val.toLowerCase();
        }
    }
];

let config  = {};
let fileName = '';

program
  .version(pkg.version)
  .option('-h, --host', 'Host for proxy')
  .option('-p, --port', 'Proxy port')
  .parse(process.argv);

let create_config = (path, config) => {

    jsonfile.writeFile(path, config, { spaces : 2 }, (err) => {
        if (err) {
            console.error(err);
        } else {

            console.log('Configuration file created');

            fs.writeFileSync(fileName, '// hello kakadu project ' + config.host);

            gulp.start('start');
        }
    });
};

let stylesPreProcessor = function() {

    switch (config.tech) {

        case 'styl':
            return stylus();
        break;

        case 'scss':
            return scss();
        break;

        case 'less':
            return less();
        break;

        default:
            console.log('Styles pre-processor error, no option in config');
    }
}

gulp.task('styles', () => {

    gulp.src(fileName)
        .pipe(plumber())
        .pipe(stylesPreProcessor())
        .pipe(autoprefixer({
            browsers: [
                "last 2 version",
                "ie >= 9",
                "Android 2.3",
                "Android >= 4",
                "Chrome >= 20",
                "Firefox >= 24",
                "Explorer >= 8",
                "iOS >= 6",
                "Opera >= 12",
                "Safari >= 6"
            ],
            cascade: true
        }))
        .pipe(groupMq())
        .pipe(gulp.dest('.'))
        .pipe(bs.stream());
});

gulp.task('proxy-start', () => {

    bs.init({

        proxy: config.host,
        serveStatic: ["./"],
        files: "./app.css",
        snippetOptions: {
            rule: {
                match: /<\/head>/i,
                fn: function (snippet, match) {
                    return '<link rel="stylesheet" type="text/css" href="/app.css"/>' + snippet + match;
                }
            }
        },
        port  : config.port
    });

    gulp.watch(fileName, ['styles']);

});

gulp.task('start', (done) => {

    runSequence('proxy-start', 'styles', done);

});

var kakadu_init = () => {

    fs.exists('kakadu.json', function (exist) {

        if (exist) {

            config = require(process.cwd() + '/kakadu.json');
            fileName = 'app.' + config.tech;
            gulp.start('start');

        } else {

            inquirer.prompt(questions).then((answers) => {

                config = {
                    host : answers.host,
                    port : answers.port,
                    tech : answers.tech
                };

                fileName = 'app.' + config.tech;

                create_config('kakadu.json', config);

            });

        }

    });
};

/*!
 * Запуск модуля
 */
kakadu_init();
<commit_msg>Добавлены настройки для BrowserSync<commit_after>#!/usr/bin/env node

'use strict';

const fs           = require('fs');
const gulp         = require('gulp');
const util         = require('gulp-util');
const plumber      = require('gulp-plumber');
const stylus       = require('gulp-stylus');
const less         = require('gulp-less');
const scss         = require('gulp-sass');
const autoprefixer = require('gulp-autoprefixer');
const groupMq      = require('gulp-group-css-media-queries');
const runSequence  = require('run-sequence');
const notifier     = require('node-notifier');
const bs           = require('browser-sync').create();
const program      = require('commander');
const jsonfile     = require('jsonfile');
const inquirer     = require('inquirer');
const pkg          = require('./package.json');
const questions    = [
    {
        type: 'input',
        name: 'host',
        message: 'Host for proxy'
    },
    {
        type    : 'input',
        name    : 'port',
        message : 'Server port',
        default : () => {
            return '7200';
        },
        validate: (value) => {

            let check = value.match(/^\d+$/);

            if (check) {
                return true;
            }

            return 'Use only numbers';
        }
    },
    {
        type: 'list',
        name: 'tech',
        message: 'What CSS pre-processor do you need?',
        choices: ['Styl', 'Scss', 'Less'],
        filter: function (val) {
            return val.toLowerCase();
        }
    }
];

let config  = {
    ghostMode: {
        clicks: true,
        forms: true,
        scroll: false
    },
    ui: false
};
let fileName = '';

program
  .version(pkg.version)
  .option('-h, --host', 'Host for proxy')
  .option('-p, --port', 'Proxy port')
  .parse(process.argv);

let create_config = (path, config) => {

    jsonfile.writeFile(path, config, { spaces : 2 }, (err) => {
        if (err) {
            console.error(err);
        } else {

            console.log('Configuration file created');

            fs.writeFileSync(fileName, '// hello kakadu project ' + config.host);

            gulp.start('start');
        }
    });
};

let stylesPreProcessor = function() {

    switch (config.tech) {

        case 'styl':
            return stylus();
        break;

        case 'scss':
            return scss();
        break;

        case 'less':
            return less();
        break;

        default:
            console.log('Styles pre-processor error, no option in config');
    }
}

gulp.task('styles', () => {

    gulp.src(fileName)
        .pipe(plumber())
        .pipe(stylesPreProcessor())
        .pipe(autoprefixer({
            browsers: [
                "last 2 version",
                "ie >= 9",
                "Android 2.3",
                "Android >= 4",
                "Chrome >= 20",
                "Firefox >= 24",
                "Explorer >= 8",
                "iOS >= 6",
                "Opera >= 12",
                "Safari >= 6"
            ],
            cascade: true
        }))
        .pipe(groupMq())
        .pipe(gulp.dest('.'))
        .pipe(bs.stream());
});

gulp.task('proxy-start', () => {

    bs.init({

        proxy: config.host,
        serveStatic: ["./"],
        files: "./app.css",
        snippetOptions: {
            rule: {
                match: /<\/head>/i,
                fn: function (snippet, match) {
                    return '<link rel="stylesheet" type="text/css" href="/app.css"/>' + snippet + match;
                }
            }
        },
        port  : config.port,
        ghostMode: {
            clicks: config.ghostMode.clicks ? config.ghostMode.clicks : false,
            forms: config.ghostMode.forms ? config.ghostMode.forms : false,
            scroll: config.ghostMode.scroll ? config.ghostMode.forms : false
        },
        ui : config.ui ? config.ui : false
    });

    gulp.watch(fileName, ['styles']);

});

gulp.task('start', (done) => {

    runSequence('proxy-start', 'styles', done);

});

var kakadu_init = () => {

    fs.exists('kakadu.json', function (exist) {

        if (exist) {

            config = require(process.cwd() + '/kakadu.json');
            fileName = 'app.' + config.tech;
            gulp.start('start');

        } else {

            inquirer.prompt(questions).then((answers) => {

                config.host = answers.host;
                config.port = answers.port;
                config.tech = answers.tech;

                fileName = 'app.' + config.tech;

                create_config('kakadu.json', config);

            });

        }

    });
};

/*!
 * Запуск модуля
 */
kakadu_init();
<|endoftext|>