<commit_before>/** @module Navigation */

import { Laptop, LaptopCarousel } from './laptops';

// Subclasses of NavigationAnimationBase
import HomeAnim from '../home/nav-shrink';
import AboutAnim from '../about/nav-shrink';
import WorkAnim from '../work/nav-shrink';

import { ease, sleep } from '../helpers';

const os = ease.outSin;

/** Coordinates navigation */
export class Navigation {

  /**
   * Initialize
   * @param {...NavigationAnimationBase} pages - the pages to include in this Navigation
   */
  constructor(...pages) {
    this.pages = Array.from(pages);

    this.shown = false;
    this.navTransitionDuration = 300;
    this.forbidPush = false;

    // Shouldn't push new state if state was arrived at by back button
    window.addEventListener('popstate', () => { this.forbidPush = true; });
    // Navigate whenever hash changes
    window.addEventListener('hashchange', () => this.navigateTo(Navigation.hash));

    // Navigation bar

    Navigation.navBarUpdate();
    // Configure navigation trigger clicked
    document.getElementById('navigation-trigger').addEventListener('click', () => this.toggle());

    // Set up laptops

    // Convert all elements with the class 'laptop3d' into laptops
    const laptopsContainer = document.getElementsByClassName('laptops-container')[0];
    const laptopElems = [...laptopsContainer.getElementsByClassName('laptop3d')];
    const laptops = laptopElems.map((l, i) => new Laptop(l, i - 1));
    // Create laptop carousel
    this.carousel = new LaptopCarousel(laptopsContainer, laptops, this);

    // Shrink other pages
    const otherpages = this.pages.filter(n => n !== this.currentPage);
    otherpages.forEach(page => page.update(1.0));
    // Make sure current page is correctly scaled, and that all other navigation UI is proper
    this.transitionUpdate(0);

    // Key bindings

    this.keyBind();
  }

  /** Set up miscellaneous key/scroll bindings for controlling navigation easily */
  keyBind() {
    // Key controls

    window.addEventListener('keydown', (e) => {
      // Open navigation when down is pressed
      if (e.key === 'ArrowDown') this.open();
      // Close navigation when escape, up, or enter is pressed
      if (e.key === 'Escape' || e.key === 'ArrowUp' || e.key === 'Enter') this.close();
      // Toggle navigation when space is pressed
      if (e.key === ' ') this.toggle();
      // Carousel left/right when navigation shown
      if (this.shown) {
        if (e.key === 'ArrowRight') this.carousel.right();
        if (e.key === 'ArrowLeft') this.carousel.left();
      }
    });

    // Trackpad/scroll controls

    window.addEventListener('wheel', (e) => {
      // Vertical scroll
      if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
        if (e.deltaY > 2) this.open();
        if (e.deltaY < -2) this.close();
      }

      e.preventDefault();
      return false;
    });
  }

  /**
   * Returns a Promise that will resolve when it's safe to animate. Only the first animation to
   * await safeToAnimate should get back a functional Promise. If multiple animations awaited
   * safeToAnimate and each was given a functional Promise, each animation would execute at the
   * same time when the Promise resolved. Instead, we return new Promise(() => {}) to all but the
   * first function to access safeToAnimate, which will never return, ensuring that only the first
   * animation to await safeToAnimate ever gets played, preventing overlap while also preventing
   * long chains of built-up animations playing in sequence in a queue.
   */
  get safeToAnimate() {
    if (this._safeToAnimate) {
      const promise = this._safeToAnimate;
      this._safeToAnimate = new Promise(() => {});
      return promise;
    }
    return undefined;
  }
  /**
   * Registers that an animation is in progress and that others should wait (by storing
   * safeToAnimate promise)
   */
  animationStarted() {
    if (typeof this._safeToAnimate === 'undefined') {
      // This promise will be resolved when an animation completes.
      // This promise can be awaited to prevent animation overlap
      this._safeToAnimate = new Promise((resolve) => {
        this.animationFinished = () => {
          resolve();
          this._safeToAnimate = undefined;
        };
      });
    } else {
      console.warn('Tried to start new animation before previous was finished');
    }
  }

  /** Returns the current URL hash without the # */
  static get hash() {
    return window.location.hash.startsWith('#')
      ? window.location.hash.substring(1)
      : window.location.hash;
  }

  /**
   * Gets the active page (the page centered on this.carousel)
   * @return {NavigationAnimationBase} the NavigationAnimationBase subclass that corresponds to the
   * page currently centered on the carousel.
   */
  get currentPage() {
    return this.pages[this.carousel.position + 1];
  }

  /**
   * Switch to a different page
   * @param {String} hash - the hash (without a leading #) to navigate to
   */
  navigateTo(hash) {
    return new Promise(async (resolve) => {
      await this.open();
      await sleep(150);
      await this.carousel.getLaptopWithHash(hash).centerSelf();
      await sleep(150);
      await this.close();
      resolve();
    });
  }

  /**
   * Move to a specific point in the shrink animation
   * @param {number} progress - progress in animation (0 being full size, 1 being fully shrunk)
   */
  transitionUpdate(progress) {
    return new Promise((resolve) => {
      // Update navigation bar opacity
      document.getElementById('navigation').style.opacity = (progress * 0.75) + 0.25;
      // Update navigation trigger opacity in the opposite direction
      document.getElementById('navigation-trigger').style.opacity = Math.max(0.25, 1 - (progress * 0.75));
      // Page shrinking into laptop
      requestAnimationFrame(() => {
        this.currentPage.update(progress);
        resolve();
      });
    });
  }

  /**
   * Show navigation
   * @return {Promise} a promise that will resolve when the opening animation has finished.
   */
  async open() {
    await this.safeToAnimate;

    // Navigation should be pushed onto the history stack for the first navigation of every visit to
    // the navigation screen, UNLESS we got there by using the back/forward buttons or with the top
    // navigation bar
    if (!this.forbidPush) this.carousel.shouldPushState = true;
    else this.forbidPush = false;

    return new Promise((resolve) => {
      // Only needs to happen if it's not already open
      if (!this.shown) {
        this.animationStarted();
        // Set up each animation frame for its correct time
        for (let i = 0; i < 1; i += (1 / 60)) {
          setTimeout(
            () => this.transitionUpdate(os(i)),       // Update to proper progress
            i * this.navTransitionDuration  // at proper time
          );
        }
        // When we're finished
        setTimeout(
          async () => { await this.transitionUpdate(1); this.animationFinished(); resolve(); },
          this.navTransitionDuration
        );
        this.shown = true;
      // If it's already open, resolve immediately
      } else {
        resolve();
      }
    });
  }

  /**
   * Hide navigation
   * @return {Promise} a promise that will resolve when the closing animation has finished.
   */
  async close() {
    await this.safeToAnimate;
    return new Promise((resolve) => {
      // Only needs to happen if it's not already open
      if (this.shown) {
        this.animationStarted();
        // Set up each animation frame for its correct time
        for (let i = 0; i < 1; i += (1 / 60)) {
          setTimeout(
            () => this.transitionUpdate(1 - os(i)),   // Update to proper progress
            i * this.navTransitionDuration  // at proper time
          );
        }
        // When we're finished
        setTimeout(
          async () => { await this.transitionUpdate(0); this.animationFinished(); resolve(); },
          this.navTransitionDuration
        );
        this.shown = false;
      // If it's already open, resolve immediately
      } else {
        resolve();
      }
    });
  }

  /**
   * Toggle navigation
   * @return {Promise} the promise returned by this.open or this.close
   */
  toggle() {
    if (this.shown) return this.close();
    return this.open();
  }

  /** Update the top nav bar to reflect the currently active page */
  static navBarUpdate() {
    const linkToHere = document.getElementById('navigation').querySelector(`a[href='#${Navigation.hash}']`);
    // un-highlight all the links
    document.getElementById('navigation').querySelectorAll('a').forEach(a => a.classList.remove('selected'));
    // re-highlight the right one
    linkToHere.classList.add('selected');
  }

}

/** Sets up a Navigation object with AboutAnim, HomeAnim, and WorkAnim as its pages, and assigns it
  * to window.navigation and window.nav
  */
export default function init() {
  window.nav = window.navigation = new Navigation(
    new AboutAnim(),
    new HomeAnim(),
    new WorkAnim()
  );
}
<commit_msg>Remove documentation of Navigation's init() function<commit_after>/** @module Navigation */

import { Laptop, LaptopCarousel } from './laptops';

// Subclasses of NavigationAnimationBase
import HomeAnim from '../home/nav-shrink';
import AboutAnim from '../about/nav-shrink';
import WorkAnim from '../work/nav-shrink';

import { ease, sleep } from '../helpers';

const os = ease.outSin;

/** Coordinates navigation */
export class Navigation {

  /**
   * Initialize
   * @param {...NavigationAnimationBase} pages - the pages to include in this Navigation
   */
  constructor(...pages) {
    this.pages = Array.from(pages);

    this.shown = false;
    this.navTransitionDuration = 300;
    this.forbidPush = false;

    // Shouldn't push new state if state was arrived at by back button
    window.addEventListener('popstate', () => { this.forbidPush = true; });
    // Navigate whenever hash changes
    window.addEventListener('hashchange', () => this.navigateTo(Navigation.hash));

    // Navigation bar

    Navigation.navBarUpdate();
    // Configure navigation trigger clicked
    document.getElementById('navigation-trigger').addEventListener('click', () => this.toggle());

    // Set up laptops

    // Convert all elements with the class 'laptop3d' into laptops
    const laptopsContainer = document.getElementsByClassName('laptops-container')[0];
    const laptopElems = [...laptopsContainer.getElementsByClassName('laptop3d')];
    const laptops = laptopElems.map((l, i) => new Laptop(l, i - 1));
    // Create laptop carousel
    this.carousel = new LaptopCarousel(laptopsContainer, laptops, this);

    // Shrink other pages
    const otherpages = this.pages.filter(n => n !== this.currentPage);
    otherpages.forEach(page => page.update(1.0));
    // Make sure current page is correctly scaled, and that all other navigation UI is proper
    this.transitionUpdate(0);

    // Key bindings

    this.keyBind();
  }

  /** Set up miscellaneous key/scroll bindings for controlling navigation easily */
  keyBind() {
    // Key controls

    window.addEventListener('keydown', (e) => {
      // Open navigation when down is pressed
      if (e.key === 'ArrowDown') this.open();
      // Close navigation when escape, up, or enter is pressed
      if (e.key === 'Escape' || e.key === 'ArrowUp' || e.key === 'Enter') this.close();
      // Toggle navigation when space is pressed
      if (e.key === ' ') this.toggle();
      // Carousel left/right when navigation shown
      if (this.shown) {
        if (e.key === 'ArrowRight') this.carousel.right();
        if (e.key === 'ArrowLeft') this.carousel.left();
      }
    });

    // Trackpad/scroll controls

    window.addEventListener('wheel', (e) => {
      // Vertical scroll
      if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
        if (e.deltaY > 2) this.open();
        if (e.deltaY < -2) this.close();
      }

      e.preventDefault();
      return false;
    });
  }

  /**
   * Returns a Promise that will resolve when it's safe to animate. Only the first animation to
   * await safeToAnimate should get back a functional Promise. If multiple animations awaited
   * safeToAnimate and each was given a functional Promise, each animation would execute at the
   * same time when the Promise resolved. Instead, we return new Promise(() => {}) to all but the
   * first function to access safeToAnimate, which will never return, ensuring that only the first
   * animation to await safeToAnimate ever gets played, preventing overlap while also preventing
   * long chains of built-up animations playing in sequence in a queue.
   */
  get safeToAnimate() {
    if (this._safeToAnimate) {
      const promise = this._safeToAnimate;
      this._safeToAnimate = new Promise(() => {});
      return promise;
    }
    return undefined;
  }
  /**
   * Registers that an animation is in progress and that others should wait (by storing
   * safeToAnimate promise)
   */
  animationStarted() {
    if (typeof this._safeToAnimate === 'undefined') {
      // This promise will be resolved when an animation completes.
      // This promise can be awaited to prevent animation overlap
      this._safeToAnimate = new Promise((resolve) => {
        this.animationFinished = () => {
          resolve();
          this._safeToAnimate = undefined;
        };
      });
    } else {
      console.warn('Tried to start new animation before previous was finished');
    }
  }

  /** Returns the current URL hash without the # */
  static get hash() {
    return window.location.hash.startsWith('#')
      ? window.location.hash.substring(1)
      : window.location.hash;
  }

  /**
   * Gets the active page (the page centered on this.carousel)
   * @return {NavigationAnimationBase} the NavigationAnimationBase subclass that corresponds to the
   * page currently centered on the carousel.
   */
  get currentPage() {
    return this.pages[this.carousel.position + 1];
  }

  /**
   * Switch to a different page
   * @param {String} hash - the hash (without a leading #) to navigate to
   */
  navigateTo(hash) {
    return new Promise(async (resolve) => {
      await this.open();
      await sleep(150);
      await this.carousel.getLaptopWithHash(hash).centerSelf();
      await sleep(150);
      await this.close();
      resolve();
    });
  }

  /**
   * Move to a specific point in the shrink animation
   * @param {number} progress - progress in animation (0 being full size, 1 being fully shrunk)
   */
  transitionUpdate(progress) {
    return new Promise((resolve) => {
      // Update navigation bar opacity
      document.getElementById('navigation').style.opacity = (progress * 0.75) + 0.25;
      // Update navigation trigger opacity in the opposite direction
      document.getElementById('navigation-trigger').style.opacity = Math.max(0.25, 1 - (progress * 0.75));
      // Page shrinking into laptop
      requestAnimationFrame(() => {
        this.currentPage.update(progress);
        resolve();
      });
    });
  }

  /**
   * Show navigation
   * @return {Promise} a promise that will resolve when the opening animation has finished.
   */
  async open() {
    await this.safeToAnimate;

    // Navigation should be pushed onto the history stack for the first navigation of every visit to
    // the navigation screen, UNLESS we got there by using the back/forward buttons or with the top
    // navigation bar
    if (!this.forbidPush) this.carousel.shouldPushState = true;
    else this.forbidPush = false;

    return new Promise((resolve) => {
      // Only needs to happen if it's not already open
      if (!this.shown) {
        this.animationStarted();
        // Set up each animation frame for its correct time
        for (let i = 0; i < 1; i += (1 / 60)) {
          setTimeout(
            () => this.transitionUpdate(os(i)),       // Update to proper progress
            i * this.navTransitionDuration  // at proper time
          );
        }
        // When we're finished
        setTimeout(
          async () => { await this.transitionUpdate(1); this.animationFinished(); resolve(); },
          this.navTransitionDuration
        );
        this.shown = true;
      // If it's already open, resolve immediately
      } else {
        resolve();
      }
    });
  }

  /**
   * Hide navigation
   * @return {Promise} a promise that will resolve when the closing animation has finished.
   */
  async close() {
    await this.safeToAnimate;
    return new Promise((resolve) => {
      // Only needs to happen if it's not already open
      if (this.shown) {
        this.animationStarted();
        // Set up each animation frame for its correct time
        for (let i = 0; i < 1; i += (1 / 60)) {
          setTimeout(
            () => this.transitionUpdate(1 - os(i)),   // Update to proper progress
            i * this.navTransitionDuration  // at proper time
          );
        }
        // When we're finished
        setTimeout(
          async () => { await this.transitionUpdate(0); this.animationFinished(); resolve(); },
          this.navTransitionDuration
        );
        this.shown = false;
      // If it's already open, resolve immediately
      } else {
        resolve();
      }
    });
  }

  /**
   * Toggle navigation
   * @return {Promise} the promise returned by this.open or this.close
   */
  toggle() {
    if (this.shown) return this.close();
    return this.open();
  }

  /** Update the top nav bar to reflect the currently active page */
  static navBarUpdate() {
    const linkToHere = document.getElementById('navigation').querySelector(`a[href='#${Navigation.hash}']`);
    // un-highlight all the links
    document.getElementById('navigation').querySelectorAll('a').forEach(a => a.classList.remove('selected'));
    // re-highlight the right one
    linkToHere.classList.add('selected');
  }

}

export default function init() {
  window.nav = window.navigation = new Navigation(
    new AboutAnim(),
    new HomeAnim(),
    new WorkAnim()
  );
}
<|endoftext|>


<commit_before>// batteries
// Copyright 2010 - 2011 Sami Samhuri <sami@samhuri.net>

// readable string representations of values
exports.repr = function(x) {
    if (x !== null && x !== undefined && typeof x.repr === 'function') return x.repr();

    if (x === null || x === undefined ||
        x instanceof Number || typeof x === 'number' ||
        x instanceof Boolean || typeof x === 'boolean' ||
        x instanceof RegExp || x.constructor === RegExp)
    {
        return String(x);
    }

    if (x instanceof String || typeof x === 'string')
        return '"' + x.replace(/"/g, '\\"') + '"';

    if (x instanceof Date || x.toUTCString)
        return 'new Date(' + (+x) + ')'; // lame

    if (Array.isArray(x))
        return '[' + x.map(repr).join(',') + ']';

    if (x instanceof Function || typeof x === 'function')
        return x.toString();

    // TODO determine how far to go with this. should we include non-enumerable props too?
    if (x instanceof Object || typeof x === 'object')
        return '{' + Object.keys(x).map(function(k) { return repr(k) + ':' + repr(x[k]); }).join(',') + '}';

    throw new Error("don't know how to represent " + x);
};
<commit_msg>update repr<commit_after>// batteries
// Copyright 2010 - 2011 Sami Samhuri <sami@samhuri.net>

// readable string representations of values
exports.repr = repr;

function repr(x) {
  if (x !== null && x !== undefined && typeof x.repr === 'function') return x.repr();

  var nativeToStringIsReadable =
         x === null
      || x === undefined
      || x instanceof Number
      || typeof x === 'number'
      || x instanceof Boolean
      || typeof x === 'boolean'
      || x instanceof RegExp
      || x.constructor === RegExp;

  if (nativeToStringIsReadable) {
    return String(x);
  }

  if (x instanceof String || typeof x === 'string') {
    return '"' + x.replace(/"/g, '\\"') + '"';
  }

  if (x instanceof Date || x.toUTCString) {
    return 'new Date(' + (+x) + ')'; // lame
  }

  if (Array.isArray(x)) {
    return '[' + x.map(repr).join(',') + ']';
  }

  if (x instanceof Function || typeof x === 'function') {
    return x.toString();
  }

  // TODO determine how far to go with this. should we include non-enumerable props too?
  if (x instanceof Object || typeof x === 'object') {
    return '{' + Object.keys(x).map(function(k) { return repr(k) + ':' + repr(x[k]); }).join(',') + '}';
  }

  throw new Error("don't know how to represent " + x);
};
<|endoftext|>


<commit_before>'use strict';

const request = require('request');
const parseString = require('xml2js').parseString;
const SSDPClient = require('node-ssdp').Client;
const ssdp = new SSDPClient();

let Roku = function (rokuIp) {
  if (!(this instanceof Roku)) {
    return new Roku(rokuIp);
  }

  if (!rokuIp || typeof rokuIp !== 'string') {
    throw new Error('Roku IP required');
  }

  this.ip = rokuIp;
  this.url = 'http://' + this.ip + ':8060/';
};

// Discover nearby Roku devices and get their IPs using SSDP
Roku.discover = function (timeout, cb) {
  if (typeof timeout === 'function') {
    cb = timeout;
    timeout = 7000; // 7 sec by default
  } else {
    timeout = Math.trunc(Number(timeout));
  }

  const devices = [];
  ssdp.on('response', function inResponse(headers, code, rinfo) {
    devices.push({
      server: headers['SERVER'],
      address: rinfo['address'],
      location: headers['LOCATION'],
      usn: headers['USN'],
    });
  });

  setTimeout(function () {
    ssdp.search('roku:ecp')
  }, timeout / 2);

  setTimeout(function () {
    ssdp.stop()
    return cb(devices);
  }, timeout);
};

Roku.keys = [
  'home', 'rev', 'fwd', 'play',
  'select', 'left', 'right', 'down',
  'up', 'back', 'replay', 'info',
  'backspace', 'enter', 'volumeDown', 'volumeUp',
  'volumeMute', 'inputTuner', 'inputHDMI1', 'inputHDMI2',
  'inputHDMI3', 'inputHDMI4', 'inputAV1', 'channelUp',
  'channelDown'
];

//todo
//Roku.prototype.delay = function () { };

Roku.prototype.press = function (key) {
  if (!key) {
    throw new Error('Key is required');
  }

  request.post(this.url + 'keypress/' + key);
};

// Launch an app by ID
Roku.prototype.launch = function (appId) {
  if (!appId) {
    throw new Error('App ID is required');
  }

  return request.post(this.url + 'launch/' + Number(appId));
};

// Fetch installed apps
Roku.prototype.apps = function (options, cb) {
  if (typeof options === 'function') {
    cb = options;
  }

  const appsUrl = this.url + 'query/apps';
  const id = options.id || null;
  const name = options.name || null;

  request.get(appsUrl, function (err, res) {
    if (err) {
      return cb(err);
    }

    parseString(res.body, function (err, result) {
      let apps = [];

      // parse the list of apps
      for (let i of result.apps.app) {
        if (i['$'].type === 'appl') {
          let meta = i['$'];

          let app = {
            id: Number(meta.id),
            name: i['_'],
            version: meta.version
          };

          apps.push(app);
        }
      }

      for (let app of apps) {
        if ((id && Number(id) === app.id) ||
          (name && name.toLowerCase() === app.name.toLowerCase())) {
          app.launch = function () {
            request.post(url + 'launch/' + this.id);
          };

          return cb(null, app);
        }
      }

      if (id || name) {
        return cb({ error: 'App not found' });
      }

      cb(null, apps);
    });
  });
};

Roku.prototype.tvChannels = function (cb) {
  request.get(this.url + 'query/tv-channels', function (err, res) {
    parseString(res.body, function (err, result) {
      if (result['tv-channels'] === '') {
        return cb([]);
      }

      const channels = result['tv-channels'].channel;
      let parsedChannels = [];

      for (let channel of channels) {
        parsedChannels.push({
          number: parseFloat(channel.number[0]),
          name: channel.name[0],
          type: channel.type[0],
          user_hidden: Boolean(channel['user-hidden'][0])
        });
      }

      cb(parsedChannels);
    });
  });
};

Roku.prototype.active = function (arg, cb) {
  const url = this.url;

  if (typeof arg !== 'string') {
    throw new Error('');
  }

  switch (arg.toLowerCase()) {
    case 'tvchannel':
      request.get(url + 'query/tv-active-channel', function (err, res) {
        parseString(res.body, function (err, result) {
          const channel = result['tv-channel'].channel[0];

          if (channel === '') {
            return cb({});
          }

          return cb({
            number: parseFloat(channel.number[0]),
            name: channel.name[0],
            type: channel.type[0],
            user_hidden: Boolean(channel['user-hidden'][0]),
            active_input: Boolean(channel['active-input'][0]),
            signal_state: channel['signal-state'][0],
            signal_mode: channel['signal-mode'][0],
            signal_quality: parseFloat(channel['signal-quality'][0]),
            signal_strength: parseFloat(channel['signal-strength'][0]),
            program_title: channel['program-title'][0],
            program_description: channel['program-description'][0],
            program_ratings: channel['program-ratings'][0],
            program_analog_audio: channel['program-analog-audio'][0],
            program_digital_audio: channel['program-digital-audio'][0],
            program_audio_languages: channel['program-audio-languages'][0],
            program_audio_formats: channel['program-audio-formats'][0],
            program_audio_language: channel['program-audio-language'][0],
            program_audio_format: channel['program-audio-format'][0],
            program_has_cc: Boolean(channel['program-has-cc'][0])
          });
        });
      }); break;
    case 'app':
      request.get(url + 'query/active-app', function (err, res) {
        parseString(res.body, function (err, result) {
          let screensaver = null;

          if (result['active-app'].screensaver) {
            screensaver = {
              name: result['active-app'].screensaver[0]._,
              id: parseFloat(result['active-app'].screensaver[0]['$'].id),
              version: result['active-app'].screensaver[0]['$'].version
            };
          }

          if (typeof result['active-app'].app[0] === 'string') {
            return cb({
              app: {
                id: null,
                name: result['active-app'].app[0],
                version: null,
                screensaver: screensaver
              }
            });
          }

          return cb({
            app: {
              id: parseFloat(result['active-app'].app[0]['$'].id),
              name: result['active-app'].app[0]._,
              version: result['active-app'].app[0]['$'].version,
              screensaver: screensaver
            }
          });
        });
      }); break;
    default:
      throw new Error('');
  }
};

Roku.prototype.type = function (text) {
  const url = this.url;

  if (typeof text !== 'string') {
    throw new Error('Text is required');
  }

  text.split('').forEach(function (char) {
    request.post(url + 'keypress/Lit_' + escape(char));
  });
};

Roku.prototype.deviceInfo = function (cb) {
  let deviceInfo1;
  let deviceInfo2;
  request.get(this.url, function (err, res) {
    deviceInfo1 = res.body;
  }).pipe(request.get(this.url + 'query/device-info', function (err, res) {
    deviceInfo2 = res.body;

    parseString(deviceInfo1, function (err, info1) {
      parseString(deviceInfo2, function (err, info2) {
        info2 = info2['device-info'];

        cb({
          spec_version: {
            major: info1.root.specVersion[0].major[0],
            minor: info1.root.specVersion[0].minor[0]
          },
          device_id: info2['device-id'][0],
          device_type: info1.root.device[0].deviceType[0],
          friendly_name: info1.root.device[0].friendlyName[0],
          manufacturer: info1.root.device[0].manufacturer[0],
          manufacturer_url: info1.root.device[0].manufacturerURL[0],
          advertising_id: info2['advertising-id'][0],
          vendor_name: info2['vendor-name'][0],
          model_description: info1.root.device[0].modelDescription[0],
          model_name: info1.root.device[0].modelName[0],
          model_region: info2['model-region'][0],
          model_number: info1.root.device[0].modelNumber[0],
          model_url: info1.root.device[0].modelURL[0],
          serial_number: info1.root.device[0].serialNumber[0],
          udn: info2.udn[0],
          screen_size: parseFloat(info2['screen-size'][0]),
          wifi_mac: info2['wifi-mac'][0],
          network_type: info2['network-type'][0],
          user_device_name: info2['user-device-name'][0],
          software_version: info2['software-version'][0],
          software_build: parseFloat(info2['software-build'][0]),
          secure_device: info2['secure-device'][0],
          language: info2.language[0],
          country: info2.country[0],
          locale: info2.locale[0],
          time_zone: info2['time-zone'][0],
          time_zone_offset: info2['time-zone-offset'][0],
          power_mode: info2['power-mode'][0],
          supports_suspend: info2['supports-suspend'][0],
          developer_enabled: info2['developer-enabled'][0],
          keyed_developer_id: info2['keyed-developer-id'][0],
          search_enabled: info2['search-enabled'][0],
          voice_search_enabled: info2['voice-search-enabled'][0],
          notifications_enabled: info2['notifications-enabled'][0],
          notifications_first_use: info2['notifications-first-use'][0],
          headphones_connected: info2['headphones-connected'][0],
          expert_pq_enabled: parseFloat(info2['expert-pq-enabled'][0]),
          service_list: function () {
            let list = [];
            for (let service of info1.root.device[0].serviceList[0].service) {
              service = {
                service_type: service.serviceType[0],
                service_id: service.serviceId[0],
                control_url: service.controlUrl ? service.controlUrl[0] : null,
                event_sub_url: service.eventSubUrl ? service.eventSubUrl[0] : null,
                scpdurl: service.SCPDURL[0]
              };

              list.push(service);
            }

            return list;
          } ()
        });
      });
    });
  }));
};

module.exports = Roku;<commit_msg>minor fixes and improvements<commit_after>/*
The MIT License (MIT)

Copyright (c) 2016 Gabriel Montalvo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

'use strict';

const request = require('request');
const parseString = require('xml2js').parseString;
const SSDPClient = require('node-ssdp').Client;
const ssdp = new SSDPClient();

let Roku = function (rokuIp) {
  if (!(this instanceof Roku)) {
    return new Roku(rokuIp);
  }

  if (!rokuIp || typeof rokuIp !== 'string') {
    throw new Error('Roku IP required');
  }

  this.ip = rokuIp;
  this.url = 'http://' + this.ip + ':8060/';
};

// Discover nearby Roku devices and get their IPs using SSDP (Make sure your firewall is disabled before testing)
Roku.discover = function (timeout, cb) {
  if (typeof timeout === 'function') {
    cb = timeout;
    timeout = 7000; // 7 sec by default
  } else {
    timeout = Math.trunc(Number(timeout));
  }

  const devices = [];
  ssdp.on('response', function inResponse(headers, code, rinfo) {
    devices.push({
      server: headers.SERVER,
      address: rinfo.ADDRESS,
      location: headers.LOCATION,
      usn: headers.USN,
    });
  });

  setTimeout(function () {
    ssdp.search('roku:ecp');
  }, timeout / 2);

  setTimeout(function () {
    ssdp.stop();
    return cb(devices);
  }, timeout);
};

Roku.keys = [
  'home', 'rev', 'fwd', 'play',
  'select', 'left', 'right', 'down',
  'up', 'back', 'replay', 'info',
  'backspace', 'enter', 'volumeDown', 'volumeUp',
  'volumeMute', 'inputTuner', 'inputHDMI1', 'inputHDMI2',
  'inputHDMI3', 'inputHDMI4', 'inputAV1', 'channelUp',
  'channelDown'
];

//todo
//Roku.prototype.delay = function () { };

Roku.prototype.press = function (key) {
  if (!key) {
    throw new Error('Key is required');
  }

  request.post(this.url + 'keypress/' + key);
};

// Launch an app by ID
Roku.prototype.launch = function (appId) {
  if (!appId) {
    throw new Error('App ID is required');
  }

  return request.post(this.url + 'launch/' + Number(appId));
};

// Fetch installed apps
Roku.prototype.apps = function (options, cb) {
  if (typeof options === 'function') {
    cb = options;
  }

  const url = this.url + 'query/apps';
  const id = options.id || null;
  const name = options.name || null;

  request.get(url, function (err, res) {
    if (err) {
      return cb(err);
    }

    parseString(res.body, function (err, result) {
      let apps = [];

      // parse the list of apps
      for (let i of result.apps.app) {
        if (i.$.type === 'appl') {
          let meta = i.$;

          let app = {
            id: Number(meta.id),
            name: i._,
            version: meta.version
          };

          apps.push(app);
        }
      }

      for (let app of apps) {
        if ((id && Number(id) === app.id) ||
          (name && name.toLowerCase() === app.name.toLowerCase())) {
          app.launch = function () {
            request.post(url + 'launch/' + this.id);
          };

          return cb(null, app);
        }
      }

      if (id || name) {
        return cb({ error: 'App not found' });
      }

      cb(null, apps);
    });
  });
};

Roku.prototype.tvChannels = function (cb) {
  request.get(this.url + 'query/tv-channels', function (err, res) {
    parseString(res.body, function (err, result) {
      if (result['tv-channels'] === '') {
        return cb([]);
      }

      const channels = result['tv-channels'].channel;
      let parsedChannels = [];

      for (let channel of channels) {
        parsedChannels.push({
          number: parseFloat(channel.number[0]),
          name: channel.name[0],
          type: channel.type[0],
          user_hidden: Boolean(channel['user-hidden'][0])
        });
      }

      cb(parsedChannels);
    });
  });
};

Roku.prototype.active = function (arg, cb) {
  const url = this.url;

  if (typeof arg !== 'string') {
    throw new Error('');
  }

  switch (arg.toLowerCase()) {
    case 'tvchannel':
      request.get(url + 'query/tv-active-channel', function (err, res) {
        parseString(res.body, function (err, result) {
          const channel = result['tv-channel'].channel[0];

          if (channel === '') {
            return cb({});
          }

          return cb({
            number: parseFloat(channel.number[0]),
            name: channel.name[0],
            type: channel.type[0],
            user_hidden: Boolean(channel['user-hidden'][0]),
            active_input: Boolean(channel['active-input'][0]),
            signal_state: channel['signal-state'][0],
            signal_mode: channel['signal-mode'][0],
            signal_quality: parseFloat(channel['signal-quality'][0]),
            signal_strength: parseFloat(channel['signal-strength'][0]),
            program_title: channel['program-title'][0],
            program_description: channel['program-description'][0],
            program_ratings: channel['program-ratings'][0],
            program_analog_audio: channel['program-analog-audio'][0],
            program_digital_audio: channel['program-digital-audio'][0],
            program_audio_languages: channel['program-audio-languages'][0],
            program_audio_formats: channel['program-audio-formats'][0],
            program_audio_language: channel['program-audio-language'][0],
            program_audio_format: channel['program-audio-format'][0],
            program_has_cc: Boolean(channel['program-has-cc'][0])
          });
        });
      }); break;
    case 'app':
      request.get(url + 'query/active-app', function (err, res) {
        parseString(res.body, function (err, result) {
          let screensaver = null;

          if (result['active-app'].screensaver) {
            screensaver = {
              name: result['active-app'].screensaver[0]._,
              id: parseFloat(result['active-app'].screensaver[0].$.id),
              version: result['active-app'].screensaver[0].$.version
            };
          }

          if (typeof result['active-app'].app[0] === 'string') {
            return cb({
              app: {
                id: null,
                name: result['active-app'].app[0],
                version: null,
                screensaver: screensaver
              }
            });
          }

          return cb({
            app: {
              id: parseFloat(result['active-app'].app[0].$.id),
              name: result['active-app'].app[0]._,
              version: result['active-app'].app[0].$.version,
              screensaver: screensaver
            }
          });
        });
      }); break;
    default:
      throw new Error('');
  }
};

Roku.prototype.type = function (text) {
  const url = this.url;

  if (typeof text !== 'string') {
    throw new Error('Text is required');
  }

  text.split('').forEach(function (char) {
    /* global escape: true */
    request.post(url + 'keypress/Lit_' + escape(char));
  });
};

Roku.prototype.deviceInfo = function (cb) {
  let deviceInfo1;
  let deviceInfo2;
  request.get(this.url, function (err, res) {
    deviceInfo1 = res.body;
  }).pipe(request.get(this.url + 'query/device-info', function (err, res) {
    deviceInfo2 = res.body;

    parseString(deviceInfo1, function (err, info1) {
      parseString(deviceInfo2, function (err, info2) {
        info2 = info2['device-info'];

        cb({
          spec_version: {
            major: info1.root.specVersion[0].major[0],
            minor: info1.root.specVersion[0].minor[0]
          },
          device_id: info2['device-id'][0],
          device_type: info1.root.device[0].deviceType[0],
          friendly_name: info1.root.device[0].friendlyName[0],
          manufacturer: info1.root.device[0].manufacturer[0],
          manufacturer_url: info1.root.device[0].manufacturerURL[0],
          advertising_id: info2['advertising-id'][0],
          vendor_name: info2['vendor-name'][0],
          model_description: info1.root.device[0].modelDescription[0],
          model_name: info1.root.device[0].modelName[0],
          model_region: info2['model-region'][0],
          model_number: info1.root.device[0].modelNumber[0],
          model_url: info1.root.device[0].modelURL[0],
          serial_number: info1.root.device[0].serialNumber[0],
          udn: info2.udn[0],
          screen_size: parseFloat(info2['screen-size'][0]),
          wifi_mac: info2['wifi-mac'][0],
          network_type: info2['network-type'][0],
          user_device_name: info2['user-device-name'][0],
          software_version: info2['software-version'][0],
          software_build: parseFloat(info2['software-build'][0]),
          secure_device: info2['secure-device'][0],
          language: info2.language[0],
          country: info2.country[0],
          locale: info2.locale[0],
          time_zone: info2['time-zone'][0],
          time_zone_offset: info2['time-zone-offset'][0],
          power_mode: info2['power-mode'][0],
          supports_suspend: info2['supports-suspend'][0],
          developer_enabled: info2['developer-enabled'][0],
          keyed_developer_id: info2['keyed-developer-id'][0],
          search_enabled: info2['search-enabled'][0],
          voice_search_enabled: info2['voice-search-enabled'][0],
          notifications_enabled: info2['notifications-enabled'][0],
          notifications_first_use: info2['notifications-first-use'][0],
          headphones_connected: info2['headphones-connected'][0],
          expert_pq_enabled: parseFloat(info2['expert-pq-enabled'][0]),
          service_list: (function () {
            let list = [];
            for (let service of info1.root.device[0].serviceList[0].service) {
              service = {
                service_type: service.serviceType[0],
                service_id: service.serviceId[0],
                control_url: service.controlUrl ? service.controlUrl[0] : null,
                event_sub_url: service.eventSubUrl ? service.eventSubUrl[0] : null,
                scpdurl: service.SCPDURL[0]
              };

              list.push(service);
            }

            return list;
          } ())
        });
      });
    });
  }));
};

module.exports = Roku;<|endoftext|>


<commit_before>'use strict'
/* eslint-env browser, webextensions */

/*
 * This content script is responsible for performing the logic of replacing
 * plain text with IPFS addresses with clickable links.
 * Loosely based on https://github.com/mdn/webextensions-examples/blob/master/emoji-substitution/substitute.js
 * Note that this is a quick&dirty PoC and may slow down browsing experience.
 * TODO: measure & improve performance
 */

;(function (alreadyLinkified) {
  if (alreadyLinkified) {
    return
  }

  // linkify lock
  window.ipfsLinkifiedDOM = true

  const urlRE = /\s+(?:\/ip(f|n)s\/|fs:|ipns:|ipfs:)[^\s+"<>]+/g

  // tags we will scan looking for un-hyperlinked IPFS addresses
  const allowedParents = [
    'abbr', 'acronym', 'address', 'applet', 'b', 'bdo', 'big', 'blockquote', 'body',
    'caption', 'center', 'cite', 'code', 'dd', 'del', 'div', 'dfn', 'dt', 'em',
    'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'iframe',
    'ins', 'kdb', 'li', 'object', 'pre', 'p', 'q', 'samp', 'small', 'span', 'strike',
    's', 'strong', 'sub', 'sup', 'td', 'th', 'tt', 'u', 'var'
  ]

  const textNodeXpath = '//text()[(parent::' + allowedParents.join(' or parent::') + ') and ' +
                  "(contains(., 'ipfs') or contains(., 'ipns')) ]"

  linkifyContainer(document.body)

  // body.appendChild(document.createTextNode('fooo /ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w bar'))
  new MutationObserver(function (mutations) {
    for (let mutation of mutations) {
      if (mutation.type === 'childList') {
        for (let addedNode of mutation.addedNodes) {
          if (addedNode.nodeType === Node.TEXT_NODE) {
            linkifyTextNode(addedNode)
          } else {
            linkifyContainer(addedNode)
          }
        }
      }
      if (mutation.type === 'characterData') {
        linkifyTextNode(mutation.target)
      }
    }
  }).observe(document.body, {
    characterData: true,
    childList: true,
    subtree: true
  })

  function linkifyContainer (container) {
    if (!container.nodeType) {
      return
    }
    if (container.className && container.className.match(/\blinkifiedIpfsAddress\b/)) {
      // prevent infinite recursion
      return
    }
    const xpathResult = document.evaluate(textNodeXpath, container, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
    let i = 0
    function continuation () {
      let node = null
      let counter = 0
      while ((node = xpathResult.snapshotItem(i++))) {
        const parent = node.parentNode
        if (!parent) continue
        // Skip styled <pre> -- often highlighted by script.
        if (parent.tagName === 'PRE' && parent.className) continue
        // Skip forms, textareas
        if (parent.isContentEditable) continue
        linkifyTextNode(node)
        if (++counter > 50) {
          return setTimeout(continuation, 0)
        }
      }
    }
    setTimeout(continuation, 0)
  }

  function normalizeHref (href) {
    console.log(href)
    // convert various variants to regular URL at the public gateway
    if (href.startsWith('ipfs:')) {
      href = href.replace('ipfs:', '/ipfs/')
    }
    if (href.startsWith('ipns:')) {
      href = href.replace('ipns:', '/ipns/')
    }
    if (href.startsWith('fs:')) {
      href = href.replace('fs:', '')
    }
    href = 'https://ipfs.io/' + href // for now just point to public gw, we will switch to custom protocol when https://github.com/ipfs/ipfs-companion/issues/164 is closed
    href = href.replace(/([^:]\/)\/+/g, '$1') // remove redundant slashes
    return href
  }

  function linkifyTextNode (node) {
    let link
    let match
    const txt = node.textContent
    let span = null
    let point = 0
    while ((match = urlRE.exec(txt))) {
      if (span == null) {
        // Create a span to hold the new text with links in it.
        span = document.createElement('span')
        span.className = 'linkifiedIpfsAddress'
      }
      // get the link without trailing dots and commas
      link = match[0].replace(/[.,]*$/, '')
      const replaceLength = link.length
      // put in text up to the link
      span.appendChild(document.createTextNode(txt.substring(point, match.index)))
      // create a link and put it in the span
      const a = document.createElement('a')
      a.className = 'linkifiedIpfsAddress'
      a.appendChild(document.createTextNode(link))
      a.setAttribute('href', normalizeHref(link.trim()))
      span.appendChild(a)
      // track insertion point
      point = match.index + replaceLength
    }
    if (span) {
      // take the text after the last link
      span.appendChild(document.createTextNode(txt.substring(point, txt.length)))
      // replace the original text with the new span
      try {
        node.parentNode.replaceChild(span, node)
      } catch (e) {
        console.error(e)
        console.log(node)
      }
    }
  }
}(window.ipfsLinkifiedDOM))
<commit_msg>fix: linkify text nodes without whitespace padding<commit_after>'use strict'
/* eslint-env browser, webextensions */

/*
 * This content script is responsible for performing the logic of replacing
 * plain text with IPFS addresses with clickable links.
 * Loosely based on https://github.com/mdn/webextensions-examples/blob/master/emoji-substitution/substitute.js
 * Note that this is a quick&dirty PoC and may slow down browsing experience.
 * TODO: measure & improve performance
 */

;(function (alreadyLinkified) {
  if (alreadyLinkified) {
    return
  }

  // linkify lock
  window.ipfsLinkifiedDOM = true

  const urlRE = /(?:\s+|^)(?:\/ip(f|n)s\/|fs:|ipns:|ipfs:)[^\s+"<>]+/g

  // tags we will scan looking for un-hyperlinked IPFS addresses
  const allowedParents = [
    'abbr', 'acronym', 'address', 'applet', 'b', 'bdo', 'big', 'blockquote', 'body',
    'caption', 'center', 'cite', 'code', 'dd', 'del', 'div', 'dfn', 'dt', 'em',
    'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'iframe',
    'ins', 'kdb', 'li', 'object', 'pre', 'p', 'q', 'samp', 'small', 'span', 'strike',
    's', 'strong', 'sub', 'sup', 'td', 'th', 'tt', 'u', 'var'
  ]

  const textNodeXpath = '//text()[(parent::' + allowedParents.join(' or parent::') + ') and ' +
                  "(contains(., 'ipfs') or contains(., 'ipns')) ]"

  linkifyContainer(document.body)

  // body.appendChild(document.createTextNode('fooo /ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w bar'))
  new MutationObserver(function (mutations) {
    for (let mutation of mutations) {
      if (mutation.type === 'childList') {
        for (let addedNode of mutation.addedNodes) {
          if (addedNode.nodeType === Node.TEXT_NODE) {
            linkifyTextNode(addedNode)
          } else {
            linkifyContainer(addedNode)
          }
        }
      }
      if (mutation.type === 'characterData') {
        linkifyTextNode(mutation.target)
      }
    }
  }).observe(document.body, {
    characterData: true,
    childList: true,
    subtree: true
  })

  function linkifyContainer (container) {
    // console.log('linkifyContainer', container)
    if (!container.nodeType) {
      return
    }
    if (container.className && container.className.match(/\blinkifiedIpfsAddress\b/)) {
      // prevent infinite recursion
      return
    }
    const xpathResult = document.evaluate(textNodeXpath, container, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
    let i = 0
    function continuation () {
      let node = null
      let counter = 0
      while ((node = xpathResult.snapshotItem(i++))) {
        const parent = node.parentNode
        if (!parent) continue
        // Skip styled <pre> -- often highlighted by script.
        if (parent.tagName === 'PRE' && parent.className) continue
        // Skip forms, textareas
        if (parent.isContentEditable) continue
        linkifyTextNode(node)
        if (++counter > 50) {
          return setTimeout(continuation, 0)
        }
      }
    }
    setTimeout(continuation, 0)
  }

  function normalizeHref (href) {
    // console.log(href)
    // convert various variants to regular URL at the public gateway
    if (href.startsWith('ipfs:')) {
      href = href.replace('ipfs:', '/ipfs/')
    }
    if (href.startsWith('ipns:')) {
      href = href.replace('ipns:', '/ipns/')
    }
    if (href.startsWith('fs:')) {
      href = href.replace('fs:', '')
    }
    href = 'https://ipfs.io/' + href // for now just point to public gw, we will switch to custom protocol when https://github.com/ipfs/ipfs-companion/issues/164 is closed
    href = href.replace(/([^:]\/)\/+/g, '$1') // remove redundant slashes
    return href
  }

  function linkifyTextNode (node) {
    // console.log('linkifyTextNode', node)
    let link
    let match
    const txt = node.textContent
    let span = null
    let point = 0
    while ((match = urlRE.exec(txt))) {
      if (span == null) {
        // Create a span to hold the new text with links in it.
        span = document.createElement('span')
        span.className = 'linkifiedIpfsAddress'
      }
      // get the link without trailing dots and commas
      link = match[0].replace(/[.,]*$/, '')
      const replaceLength = link.length
      // put in text up to the link
      span.appendChild(document.createTextNode(txt.substring(point, match.index)))
      // create a link and put it in the span
      const a = document.createElement('a')
      a.className = 'linkifiedIpfsAddress'
      a.appendChild(document.createTextNode(link))
      a.setAttribute('href', normalizeHref(link.trim()))
      span.appendChild(a)
      // track insertion point
      point = match.index + replaceLength
    }
    if (span) {
      // take the text after the last link
      span.appendChild(document.createTextNode(txt.substring(point, txt.length)))
      // replace the original text with the new span
      try {
        node.parentNode.replaceChild(span, node)
      } catch (e) {
        console.error(e)
        console.log(node)
      }
    }
  }
}(window.ipfsLinkifiedDOM))
<|endoftext|>


<commit_before>import Ember from 'ember';
import layout from '../templates/components/md-toast';
import LayoutRules from '../mixins/layout-rules';

var MdToast = Ember.Component.extend(LayoutRules, {
  layout: layout,
  tagName: 'md-toast',
  classNameBindings: ['toast.capsule:md-capsule',
    'toast.open:ng-enter-active',
    'toast.opening:ng-enter',
    'positionBinding',
    'destroyingClass'],

  didInsertElement() {
    this._super(...arguments);
    this.setupToast();
  },

  willDestroyElement() {
    this._super(...arguments);
    this.removeToast();
  },

  setupToast() {

    // TODO: this should run one frame after render, but not sure best way to implement
    Ember.run.schedule('afterRender', () => {
      Ember.run.later(this, () => {
        this.set('toast.open', true);
      }, 16);
    });

    var delay = this.get('toast.hideDelay');

    if (delay && delay > 0) {
      Ember.run.later(this, () => {
        // destroy myself if I haven't been destroyed already.
        if (this.get('toast')) {
          this.set('toast.destroying', true);
        }
      }, delay);
    }
  },

  removeToast() {
    if (this.get('actionClicked')) {
      this.sendAction('toast.resolve');
    }
  },

  rootElement: Ember.computed(function() {
    var re = this.container.lookup('application:main').get('rootElement');
    return Ember.$(re);
  }),

  positionBinding: Ember.computed('toast.position', function() {
    var position = this.get('toast.position'),
        positions = '';

    if (!position) {
      return '';
    }

    position.split(' ').forEach((p) => {
      if (p.trim().length > 0) {
        positions = positions + 'md-' + p + ' ';
      }

    });

    return positions;
  }),

  openClass: Ember.computed('toast.open', function() {
    if (this.get('toast.open')) {
      return 'ng-enter-active';
    }

    return '';
  }),

  destroyingClass: Ember.computed('toast.destroying', function() {
    if (this.get('toast.destroying')) {
      return 'ng-leave ng-leave-active';
    }

    return '';
  }),

  destroying: Ember.observer('toast.destroying', function() {
    if (!this.get('toast.destroying')) {
      return;
    }

    this.beginPropertyChanges();
    this.set('toast.open', false);
    this.set('toast.opening', false);
    this.endPropertyChanges();

    Ember.run.later(this, () => {
      this.set('toast.destroyed', true);
    }, 400);
  }),

  buttonClass: Ember.computed('toast.highlightAction', function() {

    var highlight = this.get('toast.highlightAction');

    if (highlight) {

      return 'md-action md-highlight';
    }

    return 'md-action';
  }),

  toastOpenClass: Ember.computed('toast.position', function() {
    var position = this.get('toast.position');

    return 'md-toast-open' + (position.indexOf('top') > -1 ? 'top' : 'bottom');

  }),

  actions: {
    action: function() {
      this.beginPropertyChanges();

      this.set('actionClicked', true);
      this.set('toast.destroying', true);

      this.endPropertyChanges();
    }
  }

});

export default MdToast;
<commit_msg>Add `md-toast-open-*` class when a toast is open.<commit_after>import Ember from 'ember';
import layout from '../templates/components/md-toast';
import LayoutRules from '../mixins/layout-rules';

var MdToast = Ember.Component.extend(LayoutRules, {
  layout: layout,
  tagName: 'md-toast',
  classNameBindings: ['toast.capsule:md-capsule',
    'toast.open:ng-enter-active',
    'toast.opening:ng-enter',
    'positionBinding',
    'destroyingClass'],

  didInsertElement() {
    this._super(...arguments);
    this.setupToast();
  },

  willDestroyElement() {
    this._super(...arguments);
    this.removeToast();
  },

  setupToast() {

    // TODO: this should run one frame after render, but not sure best way to implement
    Ember.run.schedule('afterRender', () => {
      Ember.run.later(this, () => {
        this.set('toast.open', true);
      }, 16);
    });

    var delay = this.get('toast.hideDelay');

    if (delay && delay > 0) {
      Ember.run.later(this, () => {
        // destroy myself if I haven't been destroyed already.
        if (this.get('toast')) {
          this.set('toast.destroying', true);
        }
      }, delay);
    }
  },

  setRootElementClass: Ember.observer('toast.open', function() {

    if (this.get('toast.open')) {
      this.get('rootElement').addClass(this.get('toastOpenClass'));
    } else {
      this.get('rootElement').removeClass(this.get('toastOpenClass'));
    }
  }),

  removeToast() {
    if (this.get('actionClicked')) {
      this.sendAction('toast.resolve');
    }
  },

  rootElement: Ember.computed(function() {
    var re = this.container.lookup('application:main').get('rootElement');
    return Ember.$(re);
  }),

  positionBinding: Ember.computed('toast.position', function() {
    var position = this.get('toast.position'),
        positions = '';

    if (!position) {
      return '';
    }

    position.split(' ').forEach((p) => {
      if (p.trim().length > 0) {
        positions = positions + 'md-' + p + ' ';
      }

    });

    return positions;
  }),

  openClass: Ember.computed('toast.open', function() {
    if (this.get('toast.open')) {
      return 'ng-enter-active';
    }

    return '';
  }),

  destroyingClass: Ember.computed('toast.destroying', function() {
    if (this.get('toast.destroying')) {
      return 'ng-leave ng-leave-active';
    }

    return '';
  }),

  destroying: Ember.observer('toast.destroying', function() {
    if (!this.get('toast.destroying')) {
      return;
    }

    this.beginPropertyChanges();
    this.set('toast.open', false);
    this.set('toast.opening', false);
    this.endPropertyChanges();

    Ember.run.later(this, () => {
      this.set('toast.destroyed', true);
    }, 400);
  }),

  buttonClass: Ember.computed('toast.highlightAction', function() {

    var highlight = this.get('toast.highlightAction');

    if (highlight) {

      return 'md-action md-highlight';
    }

    return 'md-action';
  }),

  toastOpenClass: Ember.computed('toast.position', function() {
    var position = this.get('toast.position');

    return 'md-toast-open-' + (position.indexOf('top') > -1 ? 'top' : 'bottom');

  }),

  actions: {
    action: function() {
      this.beginPropertyChanges();

      this.set('actionClicked', true);
      this.set('toast.destroying', true);

      this.endPropertyChanges();
    }
  }

});

export default MdToast;
<|endoftext|>


<commit_before>var Vue = require('vue');
var omit = require('lodash/omit');
var Cropper = require('cropperjs').default;
require('cropperjs/dist/cropper.css');

var CropperComponent = Vue.extend({
    template: `
        <div :style="style">
            <img
                v-el:img
                :src="src"
                :alt="[ alt === undefined ? 'image': alt ]"
                style="max-width: 100%;"
                :style="imgStyle"
            />
        </div>
    `,
    props: {
        'style': Object,
        'data': Object,
        'preview': String,
        'src': {
            type: String,
            required: true
        },
        'alt': String,
        'imgStyle': String,
        'dragMode': String,
        'responsive': {
            type: Boolean,
            default: true
        },
        'restore': {
            type: Boolean,
            default: true
        },
        'checkCrossOrigin': {
            type: Boolean,
            default: true
        },
        'checkOrientation': {
            type: Boolean,
            default: true
        },
        'cropBoxMovable': {
            type: Boolean,
            default: true
        },
        'cropBoxResizable': {
            type: Boolean,
            default: true
        },
        'toggleDragModeOnDblclick': {
            type: Boolean,
            default: true
        },
        'modal': {
            type: Boolean,
            default: true
        },
        'center': {
            type: Boolean,
            default: true
        },
        'highlight': {
            type: Boolean,
            default: true
        },
        'zoomOnTouch': {
            type: Boolean,
            default: true
        },
        'zoomOnWheel': {
            type: Boolean,
            default: true
        },
        'scalable': {
            type: Boolean,
            default: true
        },
        'zoomable': {
            type: Boolean,
            default: true
        },
        'guides': {
            type: Boolean,
            default: true
        },
        'background': {
            type: Boolean,
            default: true
        },
        'autoCrop': {
            type: Boolean,
            default: true
        },
        'movable': {
            type: Boolean,
            default: true
        },
        'rotatable': {
            type: Boolean,
            default: true
        },
        'viewMode': Number,
        'aspectRatio': Number,
        'autoCropArea': Number,
        'wheelZoomRatio': Number,

        // Size limitation
        'minCanvasWidth': Number,
        'minCanvasHeight': Number,
        'minCropBoxWidth': Number,
        'minCropBoxHeight': Number,
        'minContainerWidth': Number,
        'minContainerHeight': Number,

        // callbacks
        'ready': Function,
        'cropstart': Function,
        'cropmove': Function,
        'cropend': Function,
        'crop': Function,
        'zoom': Function,
    },
    ready () {
        var data = omit(this.$options.props, ['style', 'src', 'alt', 'imgStyle']);
        var props = {};
        for (var key in data) {
            if (this[key] !== undefined) {
                props[key] = this[key];
            }
        }

        this.cropper = new Cropper(this.$els.img, props);
    },
    methods: {
        reset () {
            return this.cropper.reset();
        },
        clear () {
            return this.cropper.clear();
        },
        replace (url, onlyColorChanged) {
            return this.cropper.replace(url, onlyColorChanged);
        },
        enable () {
            return this.cropper.enable();
        },
        disable () {
            return this.cropper.disable();
        },
        destroy () {
            return this.cropper.destroy();
        },
        move (offsetX, offsetY) {
            return this.cropper.move(offsetX, offsetY);
        },
        moveTo (x, y) {
            return this.cropper.moveTo(x, y);
        },
        zoom (ratio, _originalEvent) {
            return this.cropper.zoom(ratio, _originalEvent);
        },
        zoomTo (ratio, _originalEvent) {
            return this.cropper.zoomTo(ratio, _originalEvent);
        },
        rotate (degree) {
            return this.cropper.rotate(degree);
        },
        rotateTo (degree) {
            return this.cropper.rotateTo(degree);
        },
        scale (scaleX, scaleY) {
            return this.cropper.scale(scaleX, scaleY);
        },
        scaleX (scaleX) {
            return this.cropper.scaleX(scaleX);
        },
        scaleY (scaleY) {
            return this.cropper.scaleY(scaleY);
        },
        getData (rounded) {
            return this.cropper.getData(rounded);
        },
        setData (data) {
            return this.cropper.setData(data);
        },
        getContainerData () {
            return this.cropper.getContainerData();
        },
        getImageData () {
            return this.cropper.getImageData();
        },
        getCanvasData () {
            return this.cropper.getCanvasData();
        },
        setCanvasData (data) {
            return this.cropper.setCanvasData(data);
        },
        getCropBoxData () {
            return this.cropper.getCropBoxData();
        },
        setCropBoxData (data) {
            return this.cropper.setCropBoxData(data);
        },
        getCroppedCanvas (options) {
            return this.cropper.getCroppedCanvas(options);
        },
        setAspectRatio (aspectRatio) {
            return this.cropper.setAspectRatio(aspectRatio);
        },
        setDragMode () {
            return this.cropper.setDragMode();
        }
    }
});

var VueCropper = Vue.component('vue-cropper', CropperComponent);

module.exports = VueCropper;
<commit_msg>Update for vue2<commit_after>var Vue = require('vue');
var omit = require('lodash/omit');
var Cropper = require('cropperjs').default;
require('cropperjs/dist/cropper.css');

var CropperComponent = Vue.extend({
    render: function (h) {
        return h('div', { style: this.style }, [
            h('img', {
                ref: 'img',
                attrs: {
                    src: this.src,
                    alt: this.alt || 'image',
                    style: { 'max-width': '100%' },
                },
                style: this.imgStyle
            })
        ]);
    },
    props: {
        'style': Object,
        'data': Object,
        'preview': String,
        'src': {
            type: String,
            default: ''
        },
        'alt': String,
        'imgStyle': String,
        'dragMode': String,
        'responsive': {
            type: Boolean,
            default: true
        },
        'restore': {
            type: Boolean,
            default: true
        },
        'checkCrossOrigin': {
            type: Boolean,
            default: true
        },
        'checkOrientation': {
            type: Boolean,
            default: true
        },
        'cropBoxMovable': {
            type: Boolean,
            default: true
        },
        'cropBoxResizable': {
            type: Boolean,
            default: true
        },
        'toggleDragModeOnDblclick': {
            type: Boolean,
            default: true
        },
        'modal': {
            type: Boolean,
            default: true
        },
        'center': {
            type: Boolean,
            default: true
        },
        'highlight': {
            type: Boolean,
            default: true
        },
        'zoomOnTouch': {
            type: Boolean,
            default: true
        },
        'zoomOnWheel': {
            type: Boolean,
            default: true
        },
        'scalable': {
            type: Boolean,
            default: true
        },
        'zoomable': {
            type: Boolean,
            default: true
        },
        'guides': {
            type: Boolean,
            default: true
        },
        'background': {
            type: Boolean,
            default: true
        },
        'autoCrop': {
            type: Boolean,
            default: true
        },
        'movable': {
            type: Boolean,
            default: true
        },
        'rotatable': {
            type: Boolean,
            default: true
        },
        'viewMode': Number,
        'aspectRatio': Number,
        'autoCropArea': Number,
        'wheelZoomRatio': Number,

        // Size limitation
        'minCanvasWidth': Number,
        'minCanvasHeight': Number,
        'minCropBoxWidth': Number,
        'minCropBoxHeight': Number,
        'minContainerWidth': Number,
        'minContainerHeight': Number,

        // callbacks
        'ready': Function,
        'cropstart': Function,
        'cropmove': Function,
        'cropend': Function,
        'crop': Function,
        'zoom': Function,
    },
    mounted: function () {
        var data = omit(this.$options.props, ['style', 'src', 'alt', 'imgStyle']);
        var props = {};
        for (var key in data) {
            if (this[key] !== undefined) {
                props[key] = this[key];
            }
        }

        this.cropper = new Cropper(this.$refs.img, props);
    },
    methods: {
        reset () {
            return this.cropper.reset();
        },
        clear () {
            return this.cropper.clear();
        },
        replace (url, onlyColorChanged) {
            return this.cropper.replace(url, onlyColorChanged);
        },
        enable () {
            return this.cropper.enable();
        },
        disable () {
            return this.cropper.disable();
        },
        destroy () {
            return this.cropper.destroy();
        },
        move (offsetX, offsetY) {
            return this.cropper.move(offsetX, offsetY);
        },
        moveTo (x, y) {
            return this.cropper.moveTo(x, y);
        },
        zoom (ratio, _originalEvent) {
            return this.cropper.zoom(ratio, _originalEvent);
        },
        zoomTo (ratio, _originalEvent) {
            return this.cropper.zoomTo(ratio, _originalEvent);
        },
        rotate (degree) {
            return this.cropper.rotate(degree);
        },
        rotateTo (degree) {
            return this.cropper.rotateTo(degree);
        },
        scale (scaleX, scaleY) {
            return this.cropper.scale(scaleX, scaleY);
        },
        scaleX (scaleX) {
            return this.cropper.scaleX(scaleX);
        },
        scaleY (scaleY) {
            return this.cropper.scaleY(scaleY);
        },
        getData (rounded) {
            return this.cropper.getData(rounded);
        },
        setData (data) {
            return this.cropper.setData(data);
        },
        getContainerData () {
            return this.cropper.getContainerData();
        },
        getImageData () {
            return this.cropper.getImageData();
        },
        getCanvasData () {
            return this.cropper.getCanvasData();
        },
        setCanvasData (data) {
            return this.cropper.setCanvasData(data);
        },
        getCropBoxData () {
            return this.cropper.getCropBoxData();
        },
        setCropBoxData (data) {
            return this.cropper.setCropBoxData(data);
        },
        getCroppedCanvas (options) {
            return this.cropper.getCroppedCanvas(options);
        },
        setAspectRatio (aspectRatio) {
            return this.cropper.setAspectRatio(aspectRatio);
        },
        setDragMode () {
            return this.cropper.setDragMode();
        }
    }
});

var VueCropper = Vue.component('vue-cropper', CropperComponent);

module.exports = VueCropper;
<|endoftext|>


<commit_before>import Ember from 'ember';
import layout from '../templates/components/pan-zoom';

export default Ember.Component.extend({
    layout: layout,
    zoomAnimation: false,
    mousewheel: true,
    // options which match jquery.panzoom options.
    classNames: ["pan-zoom"],
    options: {
        eventNamespace: ".panzoom",
        transition: true,
        cursor: "move",
        disablePan: false,
        disableZoom: false,
        increment: 0.5,
        minScale: 1,
        maxScale: 5,
        rangeStep: 0.05,
        duration: 200,
        easing: "ease-in-out",
        contain: false,
        startTransform: undefined,
    },

    // computed alias for property
    eventNamespace: Ember.computed.alias("options.eventNamespace"),
    transition: Ember.computed.alias("options.transition"),
    cursor: Ember.computed.alias("options.cursor"),
    disablePan: Ember.computed.alias("options.disablePan"),
    disableZoom: Ember.computed.alias("options.disableZoom"),
    increment: Ember.computed.alias("options.increment"),
    minScale: Ember.computed.alias("options.minScale"),
    maxScale: Ember.computed.alias("options.maxScale"),
    rangeStep: Ember.computed.alias("options.rangeStep"),
    duration: Ember.computed.alias("options.duration"),
    easing: Ember.computed.alias("options.easing"),
    contain: Ember.computed.alias("options.contain"),
    startTransform: Ember.computed.alias("options.startTransform"),

    /*
     * @method observer option change, and set panzoom option
     */
    onOptionChanged: Ember.observer("options", function () {
        this.get("$panzoom")("options", this.get("options"));
    }),

    onMouseWheel(e) {
        e.preventDefault();
        let delta = e.delta || e.originalEvent.wheelDelta;
        let zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;
        this.get("$panzoom").panzoom('zoom', zoomOut, {
            increment: 0.1,
            animate: this.get("zoomAnimation"),
            focal: e
        });
    },

    onReset(options) {
        options = options || {};
        this.get("$panzoom").panzoom("reset", options);
    },

    didInsertElement() {
        this._super(...arguments);
        var _this = this;
        let $panzoom = this.$().panzoom({
            eventNamespace: this.get("eventNamespace"),
            transition: this.get("transition"),
            cursor: this.get("cursor"),
            disablePan: this.get("disablePan"),
            disableZoom: this.get("disableZoom"),
            increment: this.get("increment"),
            minScale: this.get("minScale"),
            maxScale: this.get("maxScale"),
            rangeStep: this.get("rangeStep"),
            duration: this.get("duration"),
            easing: this.get("easing"),
            contain: this.get("contain"),
            startTransform: this.get("startTransform"),
        })
        .on('onStart', function ($event, panzoom) {
            _this.sendAction('onStart', $event, panzoom);
        })
        .on('onChange', function ($event, panzoom) {
            _this.sendAction('onChange', panzoom);
        })
        .on('onPan', function ($event, panzoom) {
            _this.sendAction('onPan', panzoom);
        })
        .on('onEnd', function ($event, panzoom) {
            _this.sendAction('onEnd', panzoom);
        })
        .on('onReset', function ($event, panzoom) {
            _this.sendAction('onReset', panzoom);
        });
        if (this.get("mousewheel")) {
            let $parent = $panzoom.parent();
            $parent.on('mousewheel.focal', this.onMouseWheel.bind(this));
            this.set("mousewheelHandled", true);
        }
        this.set("$panzoom", $panzoom);

        // If parent controller has on, so we can listen to reset event.
        let parent = this.get("targetObject");
        if (parent && parent.on) {
            this.set("resetHandled", true);
            parent.on("reset", this.onReset.bind(this));
        }
    },

    willDestroyElement() {
        if (this.get("mousewheelHandled")) {
            let $parent = this.$().parent();
            $parent.off('mousewheel.focal', this.onMouseWheel.bind(this));
        }
        if (this.get("resetHandled")) {
            let parent = this.get("targetObject");
            parent.off("reset", this.onReset.bind(this));
        }
    },
});<commit_msg>resetDimenstions when resize happened or orientation happened.<commit_after>import Ember from 'ember';
import layout from '../templates/components/pan-zoom';

export default Ember.Component.extend({
    layout: layout,
    zoomAnimation: false,
    mousewheel: true,
    // options which match jquery.panzoom options.
    classNames: ["pan-zoom"],
    options: {
        eventNamespace: ".panzoom",
        transition: true,
        cursor: "move",
        disablePan: false,
        disableZoom: false,
        increment: 0.5,
        minScale: 1,
        maxScale: 5,
        rangeStep: 0.05,
        duration: 200,
        easing: "ease-in-out",
        contain: false,
        startTransform: undefined,
    },

    // computed alias for property
    eventNamespace: Ember.computed.alias("options.eventNamespace"),
    transition: Ember.computed.alias("options.transition"),
    cursor: Ember.computed.alias("options.cursor"),
    disablePan: Ember.computed.alias("options.disablePan"),
    disableZoom: Ember.computed.alias("options.disableZoom"),
    increment: Ember.computed.alias("options.increment"),
    minScale: Ember.computed.alias("options.minScale"),
    maxScale: Ember.computed.alias("options.maxScale"),
    rangeStep: Ember.computed.alias("options.rangeStep"),
    duration: Ember.computed.alias("options.duration"),
    easing: Ember.computed.alias("options.easing"),
    contain: Ember.computed.alias("options.contain"),
    startTransform: Ember.computed.alias("options.startTransform"),

    /*
     * @method observer option change, and set panzoom option
     */
    onOptionChanged: Ember.observer("options", function () {
        this.get("$panzoom")("options", this.get("options"));
    }),

    onMouseWheel(e) {
        e.preventDefault();
        let delta = e.delta || e.originalEvent.wheelDelta;
        let zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;
        this.get("$panzoom").panzoom('zoom', zoomOut, {
            increment: 0.1,
            animate: this.get("zoomAnimation"),
            focal: e
        });
    },

    onReset(options) {
        options = options || {};
        this.get("$panzoom").panzoom("reset", options);
    },

    onSizeChanged() {
        this.get("$panzoom").panzoom('resetDimensions');
    },

    didInsertElement() {
        this._super(...arguments);
        var _this = this;
        let $panzoom = this.$().panzoom({
            eventNamespace: this.get("eventNamespace"),
            transition: this.get("transition"),
            cursor: this.get("cursor"),
            disablePan: this.get("disablePan"),
            disableZoom: this.get("disableZoom"),
            increment: this.get("increment"),
            minScale: this.get("minScale"),
            maxScale: this.get("maxScale"),
            rangeStep: this.get("rangeStep"),
            duration: this.get("duration"),
            easing: this.get("easing"),
            contain: this.get("contain"),
            startTransform: this.get("startTransform"),
        })
        .on('panzoomstart', function ($event, panzoom, $previous, touches) {
            _this.sendAction('start', panzoom, $previous, touches);
        })
        .on('panzoomchange', function ($event, panzoom, transform) {
            _this.sendAction('change', panzoom, transform);
        })
        .on("panzoomzoom", function ($event, panzoom, scale, opts) {
            _this.sendAction('zoom', panzoom, scale, opts);
        })
        .on('panzoompan', function ($event, panzoom, x, y) {
            _this.sendAction('pan', panzoom, x, y);
        })
        .on('panzoomend', function ($event, panzoom, x, y) {
            _this.sendAction('end', panzoom, x, y);
        })
        .on('panzoomreset', function ($event, panzoom, matrix) {
            _this.sendAction('reset', panzoom, matrix);
        });

        if (this.get("mousewheel")) {
            let $parent = $panzoom.parent();
            $parent.on('mousewheel.focal', this.onMouseWheel.bind(this));
            this.set("mousewheelHandled", true);
        }
        this.set("$panzoom", $panzoom);

        // If parent controller has on, so we can listen to reset event.
        let parent = this.get("targetObject");
        if (parent && parent.on) {
            this.set("resetHandled", true);
            parent.on("reset", this.onReset.bind(this));
        }
        this.$(window).on('orientationchange', this.onSizeChanged.bind(this));
        this.$(window).on('resize', this.onSizeChanged.bind(this));
    },

    willDestroyElement() {
        if (this.get("mousewheelHandled")) {
            let $parent = this.$().parent();
            $parent.off('mousewheel.focal', this.onMouseWheel.bind(this));
        }
        if (this.get("resetHandled")) {
            let parent = this.get("targetObject");
            parent.off("reset", this.onReset.bind(this));
        }
        this.$(window).off('orientationchange', this.onSizeChanged.bind(this));
        this.$(window).off('resize', this.onSizeChanged.bind(this));
    },
});<|endoftext|>


<commit_before>/**
 * STAGGERED LIST
 *
 * A component mixin that renders list items by performing a staggered animation of their
 * entrance by listening to a "showItems" binding.
 *
 * NOTE: Right now, a "list item" is considered anything that's supplied as a direct child
 * element of the component's template block.
 */
import Ember from 'ember';
//import crossBrowserize from 'ember-stagger-swagger/utils/cross-browserize-style-object';
import setElementStyleProperty from 'ember-stagger-swagger/utils/set-element-style-property';

const {
  Mixin,
  computed,
  run,
  assert,
} = Ember;

/**
 * 2 frames per item (1 frame @ 60fps ~= 16ms) creates a noticeably staggered
 * but still-perceptively fluid motion.
 * (see: https://en.wikipedia.org/wiki/Traditional_animation#.22Shooting_on_twos.22)
 */
const DEFAULT_STAGGER_INTERVAL = 32; // ms

const CLASS_NAMES = {
  itemsHidden: 'items-hidden',
  itemsShowing: 'items-showing',
  itemsCollapsing: 'items-collapsing',
};

const STAGGER_DIRECTIONS = {
  LEFT: 'left',
  DOWN: 'down',
  RIGHT: 'right',
  UP: 'up',
};

const ANIMATION_NAME_MAP = {
  [STAGGER_DIRECTIONS.LEFT]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromRight',
    out: '__EmberStaggerSwagger__SlideAndFadeOutRight',
  },
  [STAGGER_DIRECTIONS.DOWN]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromTop',
    out: '__EmberStaggerSwagger__SlideAndFadeOutUp',
  },
  [STAGGER_DIRECTIONS.RIGHT]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromLeft',
    out: '__EmberStaggerSwagger__SlideAndFadeOutLeft',
  },
  [STAGGER_DIRECTIONS.UP]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromBottom',
    out: '__EmberStaggerSwagger__SlideAndFadeOutDown',
  },
};

const ANIMATION_NAME_PREFIXES  = [
  'webkit',
  'ms',
  'moz',
  'o',
];

export default Mixin.create({

  classNames: ['_ember-stagger-swagger_stagger-list'],

  showItems: false,

  /* MILLESECONDS */
  staggerInterval: null,

  staggerDirection: null,
  inAnimationName: null,
  outAnimationName: null,

  /**
   * Callback (to be initialized) for our animationend event listener
   */
  _onStaggerStart: null,
  _onStaggerComplete: null,

  /**
   * Array of cached "list item" elements to cache upon insertion
   */
  _listItemElems: null,

  _inAnimationName: computed('inAnimationName', 'staggerDirection', function computeInAnimationName () {
    return this.get('inAnimationName') || ANIMATION_NAME_MAP[this.get('staggerDirection')].in;
  }),

  _outAnimationName: computed ('outAnimationName', 'staggerDirection', function computeOutAnimationName () {
    return this.get('outAnimationName') || ANIMATION_NAME_MAP[this.get('staggerDirection')].out;
  }),

  currentAnimationName: computed(
    '_inAnimationName',
    '_outAnimationName',
    'showItems',
    function currentAnimationName() {
      return this.get('showItems') ? this.get('_inAnimationName') : this.get('_outAnimationName');
    }
  ),

  init () {
    this._super(...arguments);

    this._initStaggerInterval();

    assert(
      'stagger-list must have a valid `staggerDirection`',
      !!this.staggerDirection && !!ANIMATION_NAME_MAP[this.staggerDirection]
    );
  },

  didInsertElement () {
    this._super(...arguments);

    this._initStaggerAnimationFunctions();
    this._cacheListItems();
    run.scheduleOnce('afterRender', this, '_setInitialDisplayState');
  },

  /**
   * Trigger the staggering animation when something on the outside updates `showItems`
   */
  didUpdateAttrs () {
    this._super(...arguments);

    const showItems = this.get('showItems');
    const classToAdd = showItems ? CLASS_NAMES.itemsShowing : CLASS_NAMES.itemsCollapsing
    const classToRemove = showItems ? CLASS_NAMES.itemsCollapsing : CLASS_NAMES.itemsShowing;

    run.once(() => {
      this.element.classList.remove(classToRemove);
      this.element.classList.add(classToAdd);
    });
    run.scheduleOnce('afterRender', this, () => {
      this._setAnimationNameOnItems(this.get('currentAnimationName'));
    });
  },

  willDestroyElement () {
    this._super(...arguments);

    this.element.removeEventListener('animationend', this._onStaggerComplete);
    this.element.removeEventListener('webkitAnimationEnd', this._onStaggerComplete);
    this.element.removeEventListener('oAnimationEnd', this._onStaggerComplete);
    this.element.removeEventListener('msAnimationEnd', this._onStaggerComplete);

    this._listItemElems = null;
  },

  _cacheListItems () {
    this._listItemElems = Array.from(this.element.children);
  },

  _initStaggerAnimationFunctions () {

    /* AnimationEvent listener to handle keeping the list items hidden */
    this._onStaggerComplete = function onStaggerComplete (event) {

      // only update the DOM after we've finished animating all items
      const lastListItemElem = this.element.lastElementChild;

      if (Object.is(event.target, lastListItemElem)) {
        run.once(() => {
          this.element.classList.toggle(CLASS_NAMES.itemsHidden);
        });
      }

    }.bind(this);

    // this._onStaggerStart = function onStaggerStart (event) {
    //   run.once(() => {
    //
    //   });
    // }
  },

  _setInitialDisplayState () {
    if (!this.showItems) {
      this.element.classList.add(CLASS_NAMES.itemsHidden);
    }

    this._setStaggerProps();
    this.element.addEventListener('animationend', this._onStaggerComplete, false);
    this.element.addEventListener('webkitAnimationEnd', this._onStaggerComplete, false);
    this.element.addEventListener('oAnimationEnd', this._onStaggerComplete, false);
    this.element.addEventListener('msAnimationEnd', this._onStaggerComplete, false);
  },

  _setStaggerProps () {
    const interval = this.get('staggerInterval');

    let delay;
    this._listItemElems.forEach((listItemElem, idx) => {
      delay = (idx + 1) * interval;
      setElementStyleProperty(listItemElem, 'animationDelay', `${delay}ms`);  // TODO: Something more efficient than a full-prefix-list sledgehammer?
    });
  },

  _initStaggerInterval () {
    if (!this.staggerInterval) {
      this.staggerInterval = DEFAULT_STAGGER_INTERVAL;

    } else {
      assert(
        'stagger interval must be a numeric value greater than 0',
        !Number.isNaN(Number(this.staggerInterval)) && this.staggerInterval > 0
      );
    }
  },

  _setAnimationNameOnItems(currentAnimationName) {
    this._listItemElems.forEach((listItemElem, idx) => {
      setElementStyleProperty(listItemElem, 'animationName', currentAnimationName);
    });
  },

});
<commit_msg>intial pass at logic for in- and out-animation direction<commit_after>/**
 * STAGGERED LIST
 *
 * A component mixin that renders list items by performing a staggered animation of their
 * entrance by listening to a "showItems" binding.
 *
 * NOTE: Right now, a "list item" is considered anything that's supplied as a direct child
 * element of the component's template block.
 */
import Ember from 'ember';
//import crossBrowserize from 'ember-stagger-swagger/utils/cross-browserize-style-object';
import setElementStyleProperty from 'ember-stagger-swagger/utils/set-element-style-property';

const {
  Mixin,
  computed,
  run,
  assert,
} = Ember;

/**
 * 2 frames per item (1 frame @ 60fps ~= 16ms) creates a noticeably staggered
 * but still-perceptively fluid motion.
 * (see: https://en.wikipedia.org/wiki/Traditional_animation#.22Shooting_on_twos.22)
 */
const DEFAULT_STAGGER_INTERVAL = 32; // ms

const CLASS_NAMES = {
  itemsHidden: 'items-hidden',
  itemsShowing: 'items-showing',
  itemsCollapsing: 'items-collapsing',
};

const STAGGER_DIRECTIONS = {
  LEFT: 'left',
  DOWN: 'down',
  RIGHT: 'right',
  UP: 'up',
};

const ANIMATION_NAME_MAP = {
  [STAGGER_DIRECTIONS.LEFT]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromRight',
    out: '__EmberStaggerSwagger__SlideAndFadeOutRight',
    inverseDirection: STAGGER_DIRECTIONS.RIGHT,
  },
  [STAGGER_DIRECTIONS.DOWN]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromTop',
    out: '__EmberStaggerSwagger__SlideAndFadeOutUp',
    inverseDirection: STAGGER_DIRECTIONS.UP,
  },
  [STAGGER_DIRECTIONS.RIGHT]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromLeft',
    out: '__EmberStaggerSwagger__SlideAndFadeOutLeft',
    inverseDirection: STAGGER_DIRECTIONS.LEFT,
  },
  [STAGGER_DIRECTIONS.UP]: {
    in: '__EmberStaggerSwagger__SlideAndFadeInFromBottom',
    out: '__EmberStaggerSwagger__SlideAndFadeOutDown',
    inverseDirection: STAGGER_DIRECTIONS.DOWN,
  },
};

const ANIMATION_NAME_PREFIXES  = [
  'webkit',
  'ms',
  'moz',
  'o',
];

export default Mixin.create({

  classNames: ['_ember-stagger-swagger_stagger-list'],

  showItems: false,

  /* MILLESECONDS */
  staggerInterval: null,

  inDirection: null,
  inAnimationName: null,
  outAnimationName: null,

  /**
   * Callback (to be initialized) for our animationend event listener
   */
  _onStaggerStart: null,
  _onStaggerComplete: null,

  /**
   * Array of cached "list item" elements to cache upon insertion
   */
  _listItemElems: null,

  _inAnimationName: computed('inAnimationName', 'inDirection', function computeInAnimationName () {
    return this.get('inAnimationName') || ANIMATION_NAME_MAP[this.get('inDirection')].in;
  }),

  _outAnimationName: computed ('outAnimationName', 'outDirection', function computeOutAnimationName () {
    debugger;
    return this.get('outAnimationName') || ANIMATION_NAME_MAP[this.get('outDirection')].out;
  }),

  currentAnimationName: computed(
    '_inAnimationName',
    '_outAnimationName',
    'showItems',
    function currentAnimationName() {
      return this.get('showItems') ? this.get('_inAnimationName') : this.get('_outAnimationName');
    }
  ),

  init () {
    this._super(...arguments);

    this._resolveInitialStaggerInterval();
    this._resolveInitialStaggerDirections();
  },

  didInsertElement () {
    this._super(...arguments);

    this._initStaggerAnimationFunctions();
    this._cacheListItems();
    run.scheduleOnce('afterRender', this, '_prepareItemsInDOM');
  },

  /**
   * Trigger the staggering animation when something on the outside updates `showItems`
   */
  didUpdateAttrs () {
    this._super(...arguments);

    const showItems = this.get('showItems');
    const classToAdd = showItems ? CLASS_NAMES.itemsShowing : CLASS_NAMES.itemsCollapsing
    const classToRemove = showItems ? CLASS_NAMES.itemsCollapsing : CLASS_NAMES.itemsShowing;

    run.once(() => {
      this.element.classList.remove(classToRemove);
      this.element.classList.add(classToAdd);
    });
    run.scheduleOnce('afterRender', this, () => {
      this._setAnimationNameOnItems(this.get('currentAnimationName'));
    });
  },

  willDestroyElement () {
    this._super(...arguments);

    this.element.removeEventListener('animationend', this._onStaggerComplete);
    this.element.removeEventListener('webkitAnimationEnd', this._onStaggerComplete);
    this.element.removeEventListener('oAnimationEnd', this._onStaggerComplete);
    this.element.removeEventListener('msAnimationEnd', this._onStaggerComplete);

    this._listItemElems = null;
  },

  _cacheListItems () {
    this._listItemElems = Array.from(this.element.children);
  },

  _initStaggerAnimationFunctions () {

    /* AnimationEvent listener to handle keeping the list items hidden */
    this._onStaggerComplete = function onStaggerComplete (event) {

      // only update the DOM after we've finished animating all items
      const lastListItemElem = this.element.lastElementChild;

      if (Object.is(event.target, lastListItemElem)) {
        run.once(() => {
          this.element.classList.toggle(CLASS_NAMES.itemsHidden);
        });
      }

    }.bind(this);

    // this._onStaggerStart = function onStaggerStart (event) {
    //   run.once(() => {
    //
    //   });
    // }
  },

  _prepareItemsInDOM () {
    if (!this.showItems) {
      this.element.classList.add(CLASS_NAMES.itemsHidden);
    }

    this._computeAnimationDelays();
    this.element.addEventListener('animationend', this._onStaggerComplete, false);
    this.element.addEventListener('webkitAnimationEnd', this._onStaggerComplete, false);
    this.element.addEventListener('oAnimationEnd', this._onStaggerComplete, false);
    this.element.addEventListener('msAnimationEnd', this._onStaggerComplete, false);
  },

  _resolveInitialStaggerInterval () {
    if (!this.staggerInterval) {
      this.staggerInterval = DEFAULT_STAGGER_INTERVAL;

    } else {
      assert(
        'stagger interval must be a numeric value greater than 0',
        !Number.isNaN(Number(this.staggerInterval)) && this.staggerInterval > 0
      );
    }
  },

  _resolveInitialStaggerDirections () {
    debugger;
    // enforce inDirection
    assert(
      'stagger-list must have a valid `inDirection`',
      !!this.inDirection && !!ANIMATION_NAME_MAP[this.inDirection]
    );

    // if not set, set the default outDirection to the reverse of the inDirection
    if (!this.outDirection) {
      this.outDirection = ANIMATION_NAME_MAP[this.inDirection].inverseDirection;

    } else if (!ANIMATION_NAME_MAP[this.outDirection]) {
      warn(`invalid \`outDirection\`. Defaulting to the continuation of \`inDirection\`
        https://https://github.com/BrianSipple/ember-stagger-swagger#usage`
      );
      this.outDirection = this.inDirection;
    }
  },

  _computeAnimationDelays () {
    const interval = this.get('staggerInterval');

    let delay;
    this._listItemElems.forEach((listItemElem, idx) => {
      delay = (idx + 1) * interval;
      setElementStyleProperty(listItemElem, 'animationDelay', `${delay}ms`);  // TODO: Something more efficient than a full-prefix-list sledgehammer?
    });
  },

  _setAnimationNameOnItems(currentAnimationName) {
    this._listItemElems.forEach((listItemElem, idx) => {
      setElementStyleProperty(listItemElem, 'animationName', currentAnimationName);
    });
  },

});
<|endoftext|>