<commit_before>/* @flow */

import Promise from 'bluebird'
import chokidar from 'chokidar'
import fs from 'fs-extra'
import path from 'path'

import analysis from './analysis'
import * as checksumer from './checksumer'
import * as chokidarEvent from './chokidar_event'
import LocalEventBuffer from './event_buffer'
import logger from '../logger'
import * as metadata from '../metadata'
import Pouch from '../pouch'
import Prep from '../prep'

import type { Checksumer } from './checksumer'
import type { ChokidarEvent } from './chokidar_event'
import type { LocalEvent } from './event'
import type { LocalChange } from './change'
import type { Metadata } from '../metadata'
import type { Pending } from '../utils/pending' // eslint-disable-line
import type EventEmitter from 'events'

const log = logger({
  component: 'LocalWatcher'
})
log.chokidar = log.child({
  component: 'Chokidar'
})

const SIDE = 'local'

type InitialScan = {
  ids: string[],
  resolve: () => void
}

// This file contains the filesystem watcher that will trigger operations when
// a file or a folder is added/removed/changed locally.
// Operations will be added to the a common operation queue along with the
// remote operations triggered by the remoteEventWatcher.
class LocalWatcher {
  syncPath: string
  prep: Prep
  pouch: Pouch
  events: EventEmitter
  initialScan: ?InitialScan
  checksumer: Checksumer
  watcher: any // chokidar
  buffer: LocalEventBuffer<ChokidarEvent>
  ensureDirInterval: *
  pendingChanges: LocalChange[]

  constructor (syncPath: string, prep: Prep, pouch: Pouch, events: EventEmitter) {
    this.syncPath = syncPath
    this.prep = prep
    this.pouch = pouch
    this.events = events
     // TODO: Read from config
    const timeoutInMs = process.env.NODE_ENV === 'test' ? 1000 : 10000
    this.buffer = new LocalEventBuffer(timeoutInMs, this.onFlush)
    this.checksumer = checksumer.init()
    this.pendingChanges = []
  }

  ensureDirSync () {
    // This code is duplicated in local/index#start
    if (!fs.existsSync(this.syncPath)) {
      this.events.emit('syncdir-unlinked')
      throw new Error('Syncdir has been unlinked')
    }
  }

  // Start chokidar, the filesystem watcher
  // https://github.com/paulmillr/chokidar
  start () {
    log.debug('Starting...')

    this.ensureDirInterval = setInterval(this.ensureDirSync.bind(this), 5000)

    this.watcher = chokidar.watch('.', {
      // Let paths in events be relative to this base path
      cwd: this.syncPath,
      // Ignore our own .system-tmp-cozy-drive directory
      ignored: /(^|[\/\\])\.system-tmp-cozy-drive/, // eslint-disable-line no-useless-escape
      // Don't follow symlinks
      followSymlinks: false,
      // The stats object is used in methods below
      alwaysStat: true,
      // Watching on Windows seems to lock dirs with subdirs, preventing them
      // from being renamed/moved/deleted.
      usePolling: (process.platform === 'win32'),
      // Filter out artifacts from editors with atomic writes
      atomic: true,
      // Poll newly created files to detect when the write is finished
      awaitWriteFinish: {
        pollInterval: 200,
        stabilityThreshold: 1000
      },
      // With node 0.10 on linux, only polling is available
      interval: 1000,
      binaryInterval: 2000
    })

    return new Promise((resolve) => {
      for (let eventType of ['add', 'addDir', 'change', 'unlink', 'unlinkDir']) {
        this.watcher.on(eventType, (path?: string, stats?: fs.Stats) => {
          log.chokidar.debug({path}, eventType)
          log.chokidar.trace({stats})
          const newEvent = chokidarEvent.build(eventType, path, stats)
          this.buffer.push(newEvent)
          this.events.emit('buffering-start')
        })
      }

      // To detect which files&folders have been removed since the last run of
      // cozy-desktop, we keep all the paths seen by chokidar during its
      // initial scan in @paths to compare them with pouchdb database.
      this.initialScan = {ids: [], resolve}

      this.watcher
        .on('ready', () => this.buffer.switchMode('timeout'))
        .on('error', (err) => {
          if (err.message === 'watch ENOSPC') {
            log.error('Sorry, the kernel is out of inotify watches! ' +
              'See doc/usage/inotify.md for how to solve this issue.')
          } else {
            log.error({err})
          }
        })

      log.info(`Now watching ${this.syncPath}`)
    })
  }

  // TODO: Start checksuming as soon as an add/change event is buffered
  // TODO: Put flushed event batches in a queue
  async onFlush (events: ChokidarEvent[]) {
    log.debug(`Flushed ${events.length} events`)

    this.events.emit('buffering-end')
    this.ensureDirSync()
    this.events.emit('local-start')

    events = events.filter((e) => e.path !== '') // @TODO handle root dir events

    const initialScan = this.initialScan
    if (initialScan != null) {
      const ids = initialScan.ids
      events.filter((e) => e.type.startsWith('add'))
            .forEach((e) => ids.push(metadata.id(e.path)))

      await this.prependOfflineUnlinkEvents(events, initialScan)

      log.debug({initialEvents: events})
    }

    log.trace('Prepare events...')
    const preparedEvents : LocalEvent[] = await this.prepareEvents(events)
    log.trace('Done with events preparation.')

    const changes : LocalChange[] = analysis(preparedEvents, this.pendingChanges)

    // TODO: Don't even acquire lock changes list is empty
    // FIXME: Shouldn't we acquire the lock before preparing the events?
    const release = await this.pouch.lock(this)
    let target = -1
    try {
      await this.sendToPrep(changes)
      target = (await this.pouch.db.changes({limit: 1, descending: true})).last_seq
    } finally {
      this.events.emit('sync-target', target)
      release()
      this.events.emit('local-end')
    }
    if (initialScan != null) {
      initialScan.resolve()
      this.initialScan = null
    }
  }

  async prependOfflineUnlinkEvents (events: ChokidarEvent[], initialScan: InitialScan) {
    // Try to detect removed files & folders
    const docs = await this.pouch.byRecursivePathAsync('')
    const inInitialScan = (doc) =>
      initialScan.ids.indexOf(metadata.id(doc.path)) !== -1
    for (const doc of docs) {
      if (inInitialScan(doc) || doc.trashed) continue

      const event = (doc.docType === 'file')
        ? {type: 'unlink', path: doc.path, old: doc}
        : {type: 'unlinkDir', path: doc.path, old: doc}

      events.unshift(event)
    }
  }

  async prepareEvents (events: ChokidarEvent[]) : Promise<LocalEvent[]> {
    const oldMetadata = async (e: ChokidarEvent): Promise<?Metadata> => {
      if (e.old) return e.old
      if (e.type === 'unlink' || e.type === 'unlinkDir') {
        try {
          return await this.pouch.db.get(metadata.id(e.path))
        } catch (err) {
          if (err.status !== 404) log.error({path: e.path, err})
        }
      }
      return null
    }

    // @PERFOPTIM ?
    //   - db.allDocs(keys: events.pick(path))
    //   - process.exec('md5sum ' + paths.join(' '))

    return Promise.map(events, async (e: ChokidarEvent): Promise<?LocalEvent> => {
      const abspath = path.join(this.syncPath, e.path)

      const e2: Object = {
        ...e,
        old: await oldMetadata(e)
      }

      if (e.type === 'add' || e.type === 'change') {
        try {
          e2.md5sum = await this.checksum(e.path)
        } catch (err) {
          // FIXME: err.code === EISDIR => keep the event? (e.g. rm foo && mkdir foo)
          if (err.code.match(/ENOENT/)) {
            log.debug({path: e.path, ino: e.stats.ino}, 'File does not exist anymore')
            e2.wip = true
          } else {
            log.error({path: e.path, err}, 'Could not compute checksum')
            return null
          }
        }
      }

      if (e.type === 'addDir') {
        if (!await fs.exists(abspath)) {
          log.debug({path: e.path}, 'Dir does not exist anymore')
          e2.wip = true
          return null
        }
      }

      return e2
    }, {concurrency: 50})
    .filter((e: ?LocalEvent) => e != null)
  }

  // @TODO inline this.onXXX in this function
  // @TODO rename LocalChange types to prep.xxxxxx
  async sendToPrep (changes: LocalChange[]) {
    const errors: Error[] = []
    for (let c of changes) {
      try {
        switch (c.type) {
          // TODO: Inline old LocalWatcher methods
          case 'LocalDirDeletion':
            await this.onUnlinkDir(c.path)
            break
          case 'LocalFileDeletion':
            await this.onUnlinkFile(c.path)
            break
          case 'LocalDirAddition':
            await this.onAddDir(c.path, c.stats)
            break
          case 'LocalFileUpdate':
            await this.onChange(c.path, c.stats, c.md5sum)
            break
          case 'LocalFileAddition':
            await this.onAddFile(c.path, c.stats, c.md5sum)
            break
          case 'LocalFileMove':
            if (c.needRefetch) {
              c.old = await this.pouch.db.get(metadata.id(c.old.path))
              c.old.childMove = false
            }
            await this.onMoveFile(c.path, c.stats, c.md5sum, c.old)
            break
          case 'LocalDirMove':
            await this.onMoveFolder(c.path, c.stats, c.old)
            break
          default:
            throw new Error('wrong changes')
        }
      } catch (err) {
        log.error({path: c.path, err})
        errors.push(err)
      }
    }

    if (errors.length > 0) {
      throw new Error(`Could not apply all changes to Prep:\n- ${errors.map(e => e.stack).join('\n- ')}`)
    }
  }

  stop (force?: bool) {
    if (this.watcher) {
      this.watcher.close()
      this.watcher = null
    }
    clearInterval(this.ensureDirInterval)
    this.buffer.switchMode('idle')
    if (force) return Promise.resolve()
    // Give some time for awaitWriteFinish events to be fired
    return new Promise((resolve) => {
      setTimeout(resolve, 3000)
    })
  }

  // Show watched paths
  debug () {
    if (this.watcher) {
      log.info('This is the list of the paths watched by chokidar:')
      const object = this.watcher.getWatched()
      for (let dir in object) {
        var file
        const files = object[dir]
        if (dir === '..') {
          for (file of Array.from(files)) {
            log.info(`- ${dir}/${file}`)
          }
        } else {
          if (dir !== '.') { log.info(`- ${dir}`) }
          for (file of Array.from(files)) {
            log.info(`  * ${file}`)
          }
        }
      }
      log.info('--------------------------------------------------')
    } else {
      log.warn('The file system is not currrently watched')
    }
  }

  /* Helpers */
  async checksum (filePath: string): Promise<string> {
    const absPath = path.join(this.syncPath, filePath)
    return this.checksumer.push(absPath)
  }

  /* Changes */

  // New file detected
  onAddFile (filePath: string, stats: fs.Stats, md5sum: string) {
    const logError = (err) => log.error({err, path: filePath})
    const doc = metadata.buildFile(filePath, stats, md5sum)
    log.info({path: filePath}, 'file added')
    return this.prep.addFileAsync(SIDE, doc).catch(logError)
  }

  async onMoveFile (filePath: string, stats: fs.Stats, md5sum: string, old: Metadata) {
    const logError = (err) => log.error({err, path: filePath})
    const doc = metadata.buildFile(filePath, stats, md5sum)
    log.info({path: filePath}, `was moved from ${old.path}`)
    return this.prep.moveFileAsync(SIDE, doc, old).catch(logError)
  }

  onMoveFolder (folderPath: string, stats: fs.Stats, old: Metadata) {
    const logError = (err) => log.error({err, path: folderPath})
    const doc = metadata.buildDir(folderPath, stats)
    log.info({path: folderPath}, `was moved from ${old.path}`)
    return this.prep.moveFolderAsync(SIDE, doc, old).catch(logError)
  }

  // New directory detected
  onAddDir (folderPath: string, stats: fs.Stats) {
    const doc = metadata.buildDir(folderPath, stats)
    log.info({path: folderPath}, 'folder added')
    return this.prep.putFolderAsync(SIDE, doc).catch(err => log.error({err, path: folderPath}))
  }

  // File deletion detected
  //
  // It can be a file moved out. So, we wait a bit to see if a file with the
  // same checksum is added and, if not, we declare this file as deleted.
  onUnlinkFile (filePath: string) {
    log.info({path: filePath}, 'File deleted')
    return this.prep.trashFileAsync(SIDE, {path: filePath}).catch(err => log.error({err, path: filePath}))
  }

  // Folder deletion detected
  //
  // We don't want to delete a folder before files inside it. So we wait a bit
  // after chokidar event to declare the folder as deleted.
  onUnlinkDir (folderPath: string) {
    log.info({path: folderPath}, 'Folder deleted')
    return this.prep.trashFolderAsync(SIDE, {path: folderPath}).catch(err => log.error({err, path: folderPath}))
  }

  // File update detected
  onChange (filePath: string, stats: fs.Stats, md5sum: string) {
    log.info({path: filePath}, 'File changed')
    const doc = metadata.buildFile(filePath, stats, md5sum)
    return this.prep.updateFileAsync(SIDE, doc)
  }
}

export default LocalWatcher
<commit_msg>core/local/watcher: Log offline unlink events<commit_after>/* @flow */

import Promise from 'bluebird'
import chokidar from 'chokidar'
import fs from 'fs-extra'
import path from 'path'

import analysis from './analysis'
import * as checksumer from './checksumer'
import * as chokidarEvent from './chokidar_event'
import LocalEventBuffer from './event_buffer'
import logger from '../logger'
import * as metadata from '../metadata'
import Pouch from '../pouch'
import Prep from '../prep'

import type { Checksumer } from './checksumer'
import type { ChokidarEvent } from './chokidar_event'
import type { LocalEvent } from './event'
import type { LocalChange } from './change'
import type { Metadata } from '../metadata'
import type { Pending } from '../utils/pending' // eslint-disable-line
import type EventEmitter from 'events'

const log = logger({
  component: 'LocalWatcher'
})
log.chokidar = log.child({
  component: 'Chokidar'
})

const SIDE = 'local'

type InitialScan = {
  ids: string[],
  resolve: () => void
}

// This file contains the filesystem watcher that will trigger operations when
// a file or a folder is added/removed/changed locally.
// Operations will be added to the a common operation queue along with the
// remote operations triggered by the remoteEventWatcher.
class LocalWatcher {
  syncPath: string
  prep: Prep
  pouch: Pouch
  events: EventEmitter
  initialScan: ?InitialScan
  checksumer: Checksumer
  watcher: any // chokidar
  buffer: LocalEventBuffer<ChokidarEvent>
  ensureDirInterval: *
  pendingChanges: LocalChange[]

  constructor (syncPath: string, prep: Prep, pouch: Pouch, events: EventEmitter) {
    this.syncPath = syncPath
    this.prep = prep
    this.pouch = pouch
    this.events = events
     // TODO: Read from config
    const timeoutInMs = process.env.NODE_ENV === 'test' ? 1000 : 10000
    this.buffer = new LocalEventBuffer(timeoutInMs, this.onFlush)
    this.checksumer = checksumer.init()
    this.pendingChanges = []
  }

  ensureDirSync () {
    // This code is duplicated in local/index#start
    if (!fs.existsSync(this.syncPath)) {
      this.events.emit('syncdir-unlinked')
      throw new Error('Syncdir has been unlinked')
    }
  }

  // Start chokidar, the filesystem watcher
  // https://github.com/paulmillr/chokidar
  start () {
    log.debug('Starting...')

    this.ensureDirInterval = setInterval(this.ensureDirSync.bind(this), 5000)

    this.watcher = chokidar.watch('.', {
      // Let paths in events be relative to this base path
      cwd: this.syncPath,
      // Ignore our own .system-tmp-cozy-drive directory
      ignored: /(^|[\/\\])\.system-tmp-cozy-drive/, // eslint-disable-line no-useless-escape
      // Don't follow symlinks
      followSymlinks: false,
      // The stats object is used in methods below
      alwaysStat: true,
      // Watching on Windows seems to lock dirs with subdirs, preventing them
      // from being renamed/moved/deleted.
      usePolling: (process.platform === 'win32'),
      // Filter out artifacts from editors with atomic writes
      atomic: true,
      // Poll newly created files to detect when the write is finished
      awaitWriteFinish: {
        pollInterval: 200,
        stabilityThreshold: 1000
      },
      // With node 0.10 on linux, only polling is available
      interval: 1000,
      binaryInterval: 2000
    })

    return new Promise((resolve) => {
      for (let eventType of ['add', 'addDir', 'change', 'unlink', 'unlinkDir']) {
        this.watcher.on(eventType, (path?: string, stats?: fs.Stats) => {
          log.chokidar.debug({path}, eventType)
          log.chokidar.trace({stats})
          const newEvent = chokidarEvent.build(eventType, path, stats)
          this.buffer.push(newEvent)
          this.events.emit('buffering-start')
        })
      }

      // To detect which files&folders have been removed since the last run of
      // cozy-desktop, we keep all the paths seen by chokidar during its
      // initial scan in @paths to compare them with pouchdb database.
      this.initialScan = {ids: [], resolve}

      this.watcher
        .on('ready', () => this.buffer.switchMode('timeout'))
        .on('error', (err) => {
          if (err.message === 'watch ENOSPC') {
            log.error('Sorry, the kernel is out of inotify watches! ' +
              'See doc/usage/inotify.md for how to solve this issue.')
          } else {
            log.error({err})
          }
        })

      log.info(`Now watching ${this.syncPath}`)
    })
  }

  // TODO: Start checksuming as soon as an add/change event is buffered
  // TODO: Put flushed event batches in a queue
  async onFlush (events: ChokidarEvent[]) {
    log.debug(`Flushed ${events.length} events`)

    this.events.emit('buffering-end')
    this.ensureDirSync()
    this.events.emit('local-start')

    events = events.filter((e) => e.path !== '') // @TODO handle root dir events

    const initialScan = this.initialScan
    if (initialScan != null) {
      const ids = initialScan.ids
      events.filter((e) => e.type.startsWith('add'))
            .forEach((e) => ids.push(metadata.id(e.path)))

      await this.prependOfflineUnlinkEvents(events, initialScan)

      log.debug({initialEvents: events})
    }

    log.trace('Prepare events...')
    const preparedEvents : LocalEvent[] = await this.prepareEvents(events)
    log.trace('Done with events preparation.')

    const changes : LocalChange[] = analysis(preparedEvents, this.pendingChanges)

    // TODO: Don't even acquire lock changes list is empty
    // FIXME: Shouldn't we acquire the lock before preparing the events?
    const release = await this.pouch.lock(this)
    let target = -1
    try {
      await this.sendToPrep(changes)
      target = (await this.pouch.db.changes({limit: 1, descending: true})).last_seq
    } finally {
      this.events.emit('sync-target', target)
      release()
      this.events.emit('local-end')
    }
    if (initialScan != null) {
      initialScan.resolve()
      this.initialScan = null
    }
  }

  async prependOfflineUnlinkEvents (events: ChokidarEvent[], initialScan: InitialScan) {
    // Try to detect removed files & folders
    const docs = await this.pouch.byRecursivePathAsync('')
    const inInitialScan = (doc) =>
      initialScan.ids.indexOf(metadata.id(doc.path)) !== -1
    for (const doc of docs) {
      if (inInitialScan(doc) || doc.trashed) continue

      const event = (doc.docType === 'file')
        ? {type: 'unlink', path: doc.path, old: doc}
        : {type: 'unlinkDir', path: doc.path, old: doc}

      log.chokidar.debug({path: doc.path}, event.type)
      events.unshift(event)
    }
  }

  async prepareEvents (events: ChokidarEvent[]) : Promise<LocalEvent[]> {
    const oldMetadata = async (e: ChokidarEvent): Promise<?Metadata> => {
      if (e.old) return e.old
      if (e.type === 'unlink' || e.type === 'unlinkDir') {
        try {
          return await this.pouch.db.get(metadata.id(e.path))
        } catch (err) {
          if (err.status !== 404) log.error({path: e.path, err})
        }
      }
      return null
    }

    // @PERFOPTIM ?
    //   - db.allDocs(keys: events.pick(path))
    //   - process.exec('md5sum ' + paths.join(' '))

    return Promise.map(events, async (e: ChokidarEvent): Promise<?LocalEvent> => {
      const abspath = path.join(this.syncPath, e.path)

      const e2: Object = {
        ...e,
        old: await oldMetadata(e)
      }

      if (e.type === 'add' || e.type === 'change') {
        try {
          e2.md5sum = await this.checksum(e.path)
        } catch (err) {
          // FIXME: err.code === EISDIR => keep the event? (e.g. rm foo && mkdir foo)
          if (err.code.match(/ENOENT/)) {
            log.debug({path: e.path, ino: e.stats.ino}, 'File does not exist anymore')
            e2.wip = true
          } else {
            log.error({path: e.path, err}, 'Could not compute checksum')
            return null
          }
        }
      }

      if (e.type === 'addDir') {
        if (!await fs.exists(abspath)) {
          log.debug({path: e.path}, 'Dir does not exist anymore')
          e2.wip = true
          return null
        }
      }

      return e2
    }, {concurrency: 50})
    .filter((e: ?LocalEvent) => e != null)
  }

  // @TODO inline this.onXXX in this function
  // @TODO rename LocalChange types to prep.xxxxxx
  async sendToPrep (changes: LocalChange[]) {
    const errors: Error[] = []
    for (let c of changes) {
      try {
        switch (c.type) {
          // TODO: Inline old LocalWatcher methods
          case 'LocalDirDeletion':
            await this.onUnlinkDir(c.path)
            break
          case 'LocalFileDeletion':
            await this.onUnlinkFile(c.path)
            break
          case 'LocalDirAddition':
            await this.onAddDir(c.path, c.stats)
            break
          case 'LocalFileUpdate':
            await this.onChange(c.path, c.stats, c.md5sum)
            break
          case 'LocalFileAddition':
            await this.onAddFile(c.path, c.stats, c.md5sum)
            break
          case 'LocalFileMove':
            if (c.needRefetch) {
              c.old = await this.pouch.db.get(metadata.id(c.old.path))
              c.old.childMove = false
            }
            await this.onMoveFile(c.path, c.stats, c.md5sum, c.old)
            break
          case 'LocalDirMove':
            await this.onMoveFolder(c.path, c.stats, c.old)
            break
          default:
            throw new Error('wrong changes')
        }
      } catch (err) {
        log.error({path: c.path, err})
        errors.push(err)
      }
    }

    if (errors.length > 0) {
      throw new Error(`Could not apply all changes to Prep:\n- ${errors.map(e => e.stack).join('\n- ')}`)
    }
  }

  stop (force?: bool) {
    if (this.watcher) {
      this.watcher.close()
      this.watcher = null
    }
    clearInterval(this.ensureDirInterval)
    this.buffer.switchMode('idle')
    if (force) return Promise.resolve()
    // Give some time for awaitWriteFinish events to be fired
    return new Promise((resolve) => {
      setTimeout(resolve, 3000)
    })
  }

  // Show watched paths
  debug () {
    if (this.watcher) {
      log.info('This is the list of the paths watched by chokidar:')
      const object = this.watcher.getWatched()
      for (let dir in object) {
        var file
        const files = object[dir]
        if (dir === '..') {
          for (file of Array.from(files)) {
            log.info(`- ${dir}/${file}`)
          }
        } else {
          if (dir !== '.') { log.info(`- ${dir}`) }
          for (file of Array.from(files)) {
            log.info(`  * ${file}`)
          }
        }
      }
      log.info('--------------------------------------------------')
    } else {
      log.warn('The file system is not currrently watched')
    }
  }

  /* Helpers */
  async checksum (filePath: string): Promise<string> {
    const absPath = path.join(this.syncPath, filePath)
    return this.checksumer.push(absPath)
  }

  /* Changes */

  // New file detected
  onAddFile (filePath: string, stats: fs.Stats, md5sum: string) {
    const logError = (err) => log.error({err, path: filePath})
    const doc = metadata.buildFile(filePath, stats, md5sum)
    log.info({path: filePath}, 'file added')
    return this.prep.addFileAsync(SIDE, doc).catch(logError)
  }

  async onMoveFile (filePath: string, stats: fs.Stats, md5sum: string, old: Metadata) {
    const logError = (err) => log.error({err, path: filePath})
    const doc = metadata.buildFile(filePath, stats, md5sum)
    log.info({path: filePath}, `was moved from ${old.path}`)
    return this.prep.moveFileAsync(SIDE, doc, old).catch(logError)
  }

  onMoveFolder (folderPath: string, stats: fs.Stats, old: Metadata) {
    const logError = (err) => log.error({err, path: folderPath})
    const doc = metadata.buildDir(folderPath, stats)
    log.info({path: folderPath}, `was moved from ${old.path}`)
    return this.prep.moveFolderAsync(SIDE, doc, old).catch(logError)
  }

  // New directory detected
  onAddDir (folderPath: string, stats: fs.Stats) {
    const doc = metadata.buildDir(folderPath, stats)
    log.info({path: folderPath}, 'folder added')
    return this.prep.putFolderAsync(SIDE, doc).catch(err => log.error({err, path: folderPath}))
  }

  // File deletion detected
  //
  // It can be a file moved out. So, we wait a bit to see if a file with the
  // same checksum is added and, if not, we declare this file as deleted.
  onUnlinkFile (filePath: string) {
    log.info({path: filePath}, 'File deleted')
    return this.prep.trashFileAsync(SIDE, {path: filePath}).catch(err => log.error({err, path: filePath}))
  }

  // Folder deletion detected
  //
  // We don't want to delete a folder before files inside it. So we wait a bit
  // after chokidar event to declare the folder as deleted.
  onUnlinkDir (folderPath: string) {
    log.info({path: folderPath}, 'Folder deleted')
    return this.prep.trashFolderAsync(SIDE, {path: folderPath}).catch(err => log.error({err, path: folderPath}))
  }

  // File update detected
  onChange (filePath: string, stats: fs.Stats, md5sum: string) {
    log.info({path: filePath}, 'File changed')
    const doc = metadata.buildFile(filePath, stats, md5sum)
    return this.prep.updateFileAsync(SIDE, doc)
  }
}

export default LocalWatcher
<|endoftext|>


<commit_before>/*
Author: James Cryer
Company: Huddle
Last updated date: 20 Jun 2013
URL: https://github.com/Huddle/PhantomCSS
More: http://tldr.huddle.com/blog/css-testing/
*/

var fs = require('fs');

var _root = '.';
var _diffRoot = false;
var _count = 0;
var _realPath;
var _diffsToProcess = [];
var _libraryRoot = '.';
var exitStatus;
var _hideElements;
var _addLabelToFailedImage = true;
var _test_match;
var _test_exclude;

exports.screenshot = screenshot;
exports.compareAll = compareAll;
exports.compareMatched = compareMatched;
exports.init = init;
exports.update = init;
exports.turnOffAnimations = turnOffAnimations;
exports.getExitStatus = getExitStatus;

function init(options){
	casper = options.casper || casper;
	_libraryRoot = options.libraryRoot || _libraryRoot;
	_root = options.screenshotRoot || _root;
	_diffRoot = options.failedComparisonsRoot || _diffRoot;
	_fileNameGetter = options.fileNameGetter || _fileNameGetter;

	_onPass = options.onPass || _onPass;
	_onFail = options.onFail || _onFail;
	_onTimeout = options.onTimeout || _onTimeout;
	_onComplete = options.onComplete || options.report || _onComplete;

	_hideElements = options.hideElements;

	if(options.addLabelToFailedImage !== undefined){
		_addLabelToFailedImage = options.addLabelToFailedImage;
	}
}

function turnOffAnimations(){
	console.log('Turning off animations');
	casper.evaluate(function turnOffAnimations(){
		window.addEventListener('load', function(){

			var css = document.createElement("style");
			css.type = "text/css";
			css.innerHTML = "* { -webkit-transition: none !important; transition: none !important; }";
			document.body.appendChild(css);

			if(jQuery){
				$.fx.off = true;
			}
		},false);
	});
}

function _fileNameGetter(root, fileName){
	var name;

	fileName = fileName || "screenshot";
	name = root + fs.separator + fileName + "_" + _count++;

	if(fs.isFile(name+'.png')){
		return name+'.diff.png';
	} else {
		return name+'.png';
	}
}

function screenshot(selector, timeToWait, hideSelector, fileName){
	casper.captureBase64('png'); // force pre-render
	casper.wait(timeToWait || 250, function(){

		if(hideSelector || _hideElements){
			casper.evaluate(function(s1, s2){
				if(s1){
					$(s1).css('visibility', 'hidden');
				}
				$(s2).css('visibility', 'hidden');
			}, {
				s1: _hideElements,
				s2: hideSelector
			});
		}

		try{
			casper.captureSelector( _fileNameGetter(_root, fileName) , selector);
		}
		catch(ex){
			console.log("Screenshot FAILED: " + ex.message);
		}

	}); // give a bit of time for all the images appear
}

function asyncCompare(one, two, func){

	if(!casper.evaluate(function(){ return window._imagediff_;})){
		initClient();
	}

	casper.fill('form#image-diff', {
		'one': one,
		'two': two
	});

	casper.evaluate(function(filename){
		window._imagediff_.run( filename );
	}, {
		label: _addLabelToFailedImage ? one : false
	});

	casper.waitFor(
		function check() {
			return this.evaluate(function(){
				return window._imagediff_.hasResult;
			});
		},
		function () {

			var mismatch = casper.evaluate(function(){
				return window._imagediff_.getResult();
			});

			if(Number(mismatch)){
				func(false, mismatch);
			} else {
				func(true);
			}

		}, function(){
			func(false);
		},
		10000
	);
}

function getDiffs (path){

	var filePath;

	if(({'..':1,'.':1})[path]){ return true; }

	if(_realPath){
		_realPath += fs.separator + path;
	} else {
		_realPath = path;
	}

	filePath = _realPath;

	if(fs.isDirectory(_realPath) ){
		fs.list(_realPath).forEach(getDiffs);
	} else {
		if( /\.diff\./.test(path.toLowerCase()) ){
			if(_test_match){
				if( _test_match.test(_realPath.toLowerCase()) ){
					if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
						console.log('Analysing', _realPath);
						_diffsToProcess.push(filePath);
					}
				}
			} else {
				if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
					_diffsToProcess.push(filePath);
				}
			}
		}
	}

	_realPath = _realPath.replace(fs.separator + path, '');
}

function compareMatched(match, exclude){
	_test_match = typeof match === 'string' ? new RegExp(match) : match;
	compareAll(exclude);
}

function compareAll(exclude){
	var tests = [];
	var fails = 0;
	var errors = 0;

	_test_exclude = typeof exclude === 'string' ? new RegExp(exclude) : exclude;
	_realPath = undefined;

	_diffsToProcess = [];

	getDiffs(_root);

	_diffsToProcess.forEach(function(file){
		var baseFile = file.replace('.diff', '');
		var test = {
			filename: baseFile
		};

		if(!fs.isFile(baseFile)) {
			test.error = true;
			errors++;
			tests.push(test);
		} else {
			casper.
			thenOpen ( "resemblejscontainer.html" , function (){

				asyncCompare(baseFile, file, function(isSame, mismatch){

					if(!isSame){

						test.fail = true;
						fails++;

						if(mismatch){
							test.mismatch = mismatch;
							_onFail(test);
						} else {
							_onTimeout(test);
						}

						casper.waitFor(
							function check() {
								return casper.evaluate(function(){
									return window._imagediff_.hasImage;
								});
							},
							function () {
								var failFile, safeFileName, increment;

								if(_diffRoot){
									// flattened structure for failed diffs so that it is easier to preview
									failFile = _diffRoot + fs.separator + file.split(fs.separator).pop().replace('.diff.png', '');
									safeFileName = failFile;
									increment = 0;

									while ( fs.isFile(safeFileName+'.fail.png') ){
										increment++;
										safeFileName = failFile+'.'+increment;
									}

									failFile = safeFileName + '.fail.png';

									casper.captureSelector(failFile, 'img');
								}

								casper.captureSelector(file.replace('.diff.png', '.fail.png'), 'img');

								casper.evaluate(function(){
									window._imagediff_.hasImage = false;
								});

							}, function(){},
							10000
						);
					} else {
						_onPass(test);
					}

					tests.push(test);
				});
			});
		}
	});

	casper.then(function(){
		casper.waitFor(function(){
			return _diffsToProcess.length === tests.length;
		}, function(){
			_onComplete(tests, fails, errors);
		}, function(){},
		10000);
	});
}

function initClient(){

	casper.page.injectJs(_libraryRoot+fs.separator+'resemble.js');

	casper.evaluate(function(){

		var result;

		var div = document.createElement('div');

		// this is a bit of hack, need to get images into browser for analysis
		div.style = "display:block;position:absolute;border:0;top:-1px;left:-1px;height:1px;width:1px;overflow:hidden;";
		div.innerHTML = '<form id="image-diff">'+
			'<input type="file" id="image-diff-one" name="one"/>'+
			'<input type="file" id="image-diff-two" name="two"/>'+
		'</form>

';
		document.body.appendChild(div);

		window._imagediff_ = {
			hasResult: false,
			hasImage: false,
			run: run,
			getResult: function(){
				window._imagediff_.hasResult = false;
				return result;
			}
		};

		function run(label){

			function render(data){
				document.getElementById('image-diff').innerHTML = '';
				window._imagediff_.hasImage = true;
			}

			resemble(document.getElementById('image-diff-one').files[0]).
				compareTo(document.getElementById('image-diff-two').files[0]).
				ignoreAntialiasing(). // <-- muy importante
				onComplete(function(data){
					var diffImage;

					if(Number(data.misMatchPercentage) > 0.05){
						result = data.misMatchPercentage;
					} else {
						result = false;
					}

					window._imagediff_.hasResult = true;

					if(Number(data.misMatchPercentage) > 0.05){
						render(data);
					}

				});
		}
	});
}

function _onPass(test){
	// console.log('.'); just for progress indication
}
function _onFail(test){
	console.log('FAILED: ('+test.mismatch+'% mismatch)', test.filename, '\n');
}
function _onTimeout(test){
	console.log('TIMEOUT: ', test.filename, '\n');
}
function _onComplete(tests, noOfFails, noOfErrors){

	if( tests.length === 0){
		console.log("\nMust be your first time?");
		console.log("Some screenshots have been generated in the directory " + _root);
		console.log("This is your 'baseline', check the images manually. If they're wrong, delete the images.");
		console.log("The next time you run these tests, new screenshots will be taken.  These screenshots will be compared to the original.");
		console.log('If they are different, PhantomCSS will report a failure.');
	} else {

		console.log("\nPhantomCSS found: " + tests.length + " tests.");

		if(noOfFails === 0){
			console.log("None of them failed. Which is good right?");
			console.log("If you want to make them fail, go change some CSS - weirdo.");
		} else {
			console.log(noOfFails + ' of them failed.');
			console.log('PhantomCSS has created some images that try to show the difference (in the directory '+_diffRoot+'). Fuchsia colored pixels indicate a difference betwen the new and old screenshots.');
		}

		if(noOfErrors !== 0){
			console.log("There were " + noOfErrors + "errors.  Is it possible that a baseline image was deleted but not the diff?");
		}

		exitStatus = noOfErrors+noOfFails;
	}
}

function getExitStatus() {
	return exitStatus;
}
<commit_msg>New screenshot method signature to allow filename as the second parameter.<commit_after>/*
Author: James Cryer
Company: Huddle
Last updated date: 06 Aug 2013
URL: https://github.com/Huddle/PhantomCSS
More: http://tldr.huddle.com/blog/css-testing/
*/

var fs = require('fs');

var _root = '.';
var _diffRoot = false;
var _count = 0;
var _realPath;
var _diffsToProcess = [];
var _libraryRoot = '.';
var exitStatus;
var _hideElements;
var _addLabelToFailedImage = true;
var _test_match;
var _test_exclude;

exports.screenshot = screenshot;
exports.compareAll = compareAll;
exports.compareMatched = compareMatched;
exports.init = init;
exports.update = init;
exports.turnOffAnimations = turnOffAnimations;
exports.getExitStatus = getExitStatus;

function init(options){
	casper = options.casper || casper;
	_libraryRoot = options.libraryRoot || _libraryRoot;
	_root = options.screenshotRoot || _root;
	_diffRoot = options.failedComparisonsRoot || _diffRoot;
	_fileNameGetter = options.fileNameGetter || _fileNameGetter;

	_onPass = options.onPass || _onPass;
	_onFail = options.onFail || _onFail;
	_onTimeout = options.onTimeout || _onTimeout;
	_onComplete = options.onComplete || options.report || _onComplete;

	_hideElements = options.hideElements;

	if(options.addLabelToFailedImage !== undefined){
		_addLabelToFailedImage = options.addLabelToFailedImage;
	}
}

function turnOffAnimations(){
	console.log('Turning off animations');
	casper.evaluate(function turnOffAnimations(){
		window.addEventListener('load', function(){

			var css = document.createElement("style");
			css.type = "text/css";
			css.innerHTML = "* { -webkit-transition: none !important; transition: none !important; }";
			document.body.appendChild(css);

			if(jQuery){
				$.fx.off = true;
			}
		},false);
	});
}

function _fileNameGetter(root, fileName){
	var name;

	fileName = fileName || "screenshot";
	name = root + fs.separator + fileName + "_" + _count++;

	if(fs.isFile(name+'.png')){
		return name+'.diff.png';
	} else {
		return name+'.png';
	}
}

function screenshot(selector, timeToWait, hideSelector, fileName){

	if(isNaN(Number(timeToWait)) && typeof timeToWait === 'string'){
		fileName = timeToWait;
		timeToWait = void 0;
	}

	casper.captureBase64('png'); // force pre-render
	casper.wait(timeToWait || 250, function(){

		if(hideSelector || _hideElements){
			casper.evaluate(function(s1, s2){
				if(s1){
					$(s1).css('visibility', 'hidden');
				}
				$(s2).css('visibility', 'hidden');
			}, {
				s1: _hideElements,
				s2: hideSelector
			});
		}

		try{
			casper.captureSelector( _fileNameGetter(_root, fileName) , selector);
		}
		catch(ex){
			console.log("Screenshot FAILED: " + ex.message);
		}

	}); // give a bit of time for all the images appear
}

function asyncCompare(one, two, func){

	if(!casper.evaluate(function(){ return window._imagediff_;})){
		initClient();
	}

	casper.fill('form#image-diff', {
		'one': one,
		'two': two
	});

	casper.evaluate(function(filename){
		window._imagediff_.run( filename );
	}, {
		label: _addLabelToFailedImage ? one : false
	});

	casper.waitFor(
		function check() {
			return this.evaluate(function(){
				return window._imagediff_.hasResult;
			});
		},
		function () {

			var mismatch = casper.evaluate(function(){
				return window._imagediff_.getResult();
			});

			if(Number(mismatch)){
				func(false, mismatch);
			} else {
				func(true);
			}

		}, function(){
			func(false);
		},
		10000
	);
}

function getDiffs (path){

	var filePath;

	if(({'..':1,'.':1})[path]){ return true; }

	if(_realPath){
		_realPath += fs.separator + path;
	} else {
		_realPath = path;
	}

	filePath = _realPath;

	if(fs.isDirectory(_realPath) ){
		fs.list(_realPath).forEach(getDiffs);
	} else {
		if( /\.diff\./.test(path.toLowerCase()) ){
			if(_test_match){
				if( _test_match.test(_realPath.toLowerCase()) ){
					if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
						console.log('Analysing', _realPath);
						_diffsToProcess.push(filePath);
					}
				}
			} else {
				if( !(_test_exclude && _test_exclude.test(_realPath.toLowerCase())) ){
					_diffsToProcess.push(filePath);
				}
			}
		}
	}

	_realPath = _realPath.replace(fs.separator + path, '');
}

function compareMatched(match, exclude){
	_test_match = typeof match === 'string' ? new RegExp(match) : match;
	compareAll(exclude);
}

function compareAll(exclude){
	var tests = [];
	var fails = 0;
	var errors = 0;

	_test_exclude = typeof exclude === 'string' ? new RegExp(exclude) : exclude;
	_realPath = undefined;

	_diffsToProcess = [];

	getDiffs(_root);

	_diffsToProcess.forEach(function(file){
		var baseFile = file.replace('.diff', '');
		var test = {
			filename: baseFile
		};

		if(!fs.isFile(baseFile)) {
			test.error = true;
			errors++;
			tests.push(test);
		} else {
			casper.
			thenOpen ( _libraryRoot+fs.separator+"resemblejscontainer.html" , function (){

				asyncCompare(baseFile, file, function(isSame, mismatch){

					if(!isSame){

						test.fail = true;
						fails++;

						if(mismatch){
							test.mismatch = mismatch;
							_onFail(test);
						} else {
							_onTimeout(test);
						}

						casper.waitFor(
							function check() {
								return casper.evaluate(function(){
									return window._imagediff_.hasImage;
								});
							},
							function () {
								var failFile, safeFileName, increment;

								if(_diffRoot){
									// flattened structure for failed diffs so that it is easier to preview
									failFile = _diffRoot + fs.separator + file.split(fs.separator).pop().replace('.diff.png', '');
									safeFileName = failFile;
									increment = 0;

									while ( fs.isFile(safeFileName+'.fail.png') ){
										increment++;
										safeFileName = failFile+'.'+increment;
									}

									failFile = safeFileName + '.fail.png';

									casper.captureSelector(failFile, 'img');
								}

								casper.captureSelector(file.replace('.diff.png', '.fail.png'), 'img');

								casper.evaluate(function(){
									window._imagediff_.hasImage = false;
								});

							}, function(){},
							10000
						);
					} else {
						_onPass(test);
					}

					tests.push(test);
				});
			});
		}
	});

	casper.then(function(){
		casper.waitFor(function(){
			return _diffsToProcess.length === tests.length;
		}, function(){
			_onComplete(tests, fails, errors);
		}, function(){},
		10000);
	});
}

function initClient(){

	casper.page.injectJs(_libraryRoot+fs.separator+'resemble.js');

	casper.evaluate(function(){

		var result;

		var div = document.createElement('div');

		// this is a bit of hack, need to get images into browser for analysis
		div.style = "display:block;position:absolute;border:0;top:-1px;left:-1px;height:1px;width:1px;overflow:hidden;";
		div.innerHTML = '<form id="image-diff">'+
			'<input type="file" id="image-diff-one" name="one"/>'+
			'<input type="file" id="image-diff-two" name="two"/>'+
		'</form>

';
		document.body.appendChild(div);

		window._imagediff_ = {
			hasResult: false,
			hasImage: false,
			run: run,
			getResult: function(){
				window._imagediff_.hasResult = false;
				return result;
			}
		};

		function run(label){

			function render(data){
				document.getElementById('image-diff').innerHTML = '';
				window._imagediff_.hasImage = true;
			}

			resemble(document.getElementById('image-diff-one').files[0]).
				compareTo(document.getElementById('image-diff-two').files[0]).
				ignoreAntialiasing(). // <-- muy importante
				onComplete(function(data){
					var diffImage;

					if(Number(data.misMatchPercentage) > 0.05){
						result = data.misMatchPercentage;
					} else {
						result = false;
					}

					window._imagediff_.hasResult = true;

					if(Number(data.misMatchPercentage) > 0.05){
						render(data);
					}

				});
		}
	});
}

function _onPass(test){
	// console.log('.'); just for progress indication
}
function _onFail(test){
	console.log('FAILED: ('+test.mismatch+'% mismatch)', test.filename, '\n');
}
function _onTimeout(test){
	console.log('TIMEOUT: ', test.filename, '\n');
}
function _onComplete(tests, noOfFails, noOfErrors){

	if( tests.length === 0){
		console.log("\nMust be your first time?");
		console.log("Some screenshots have been generated in the directory " + _root);
		console.log("This is your 'baseline', check the images manually. If they're wrong, delete the images.");
		console.log("The next time you run these tests, new screenshots will be taken.  These screenshots will be compared to the original.");
		console.log('If they are different, PhantomCSS will report a failure.');
	} else {

		console.log("\nPhantomCSS found: " + tests.length + " tests.");

		if(noOfFails === 0){
			console.log("None of them failed. Which is good right?");
			console.log("If you want to make them fail, go change some CSS - weirdo.");
		} else {
			console.log(noOfFails + ' of them failed.');
			console.log('PhantomCSS has created some images that try to show the difference (in the directory '+_diffRoot+'). Fuchsia colored pixels indicate a difference betwen the new and old screenshots.');
		}

		if(noOfErrors !== 0){
			console.log("There were " + noOfErrors + "errors.  Is it possible that a baseline image was deleted but not the diff?");
		}

		exitStatus = noOfErrors+noOfFails;
	}
}

function getExitStatus() {
	return exitStatus;
}
<|endoftext|>


<commit_before>blkUI.factory('blockUI', function(blockUIConfig, $timeout, blockUIUtils, $document) {

  var $body = $document.find('body');

  function BlockUI(id) {

    var self = this;

    var state = {
      id: id,
      blockCount: 0,
      message: blockUIConfig.message,
      blocking: false
    }, startPromise, doneCallbacks = [];

    this._id = id;

    this._refs = 0;

    this.start = function(message) {

      if(state.blockCount > 0) {
        message = message || state.message || blockUIConfig.message;
      } else {
        message = message || blockUIConfig.message;
      }

      state.message = message;

      state.blockCount++;

      // Check if the focused element is part of the block scope

      var $ae = angular.element($document[0].activeElement);

      if($ae.length && blockUIUtils.isElementInBlockScope($ae, self)) {

        // Let the active element lose focus and store a reference
        // to restore focus when we're done (reset)

        self._restoreFocus = $ae[0];

        // https://github.com/McNull/angular-block-ui/issues/13
        // http://stackoverflow.com/questions/22698058/apply-already-in-progress-error-when-using-typeahead-plugin-found-to-be-relate
        // Queue the blur after any ng-blur expression.

        $timeout(function() {
          // Ensure we still need to blur
          // Don't restore if active element is body, since this causes IE to switch windows (see http://tjvantoll.com/2013/08/30/bugs-with-document-activeelement-in-internet-explorer/)
          if (self._restoreFocus && self._restoreFocus !== $body[0]) {
            self._restoreFocus.blur();
          }
        });
      }

      if (!startPromise && blockUIConfig.delay !== 0) {
        startPromise = $timeout(block, blockUIConfig.delay);
      } else if (blockUIConfig.delay === 0) {
        block();
      }

      function block () {
        startPromise = null;
        state.blocking = true;
      }
    };

    this._cancelStartTimeout = function() {
      if (startPromise) {
        $timeout.cancel(startPromise);
        startPromise = null;
      }
    };

    this.stop = function() {
      state.blockCount = Math.max(0, --state.blockCount);

      if (state.blockCount === 0) {
        self.reset(true);
      }
    };

    this.isBlocking = function () {
        return state.blocking;
    };

    this.message = function(value) {
      state.message = value;
    };

    this.pattern = function(regexp) {
      if (regexp !== undefined) {
        self._pattern = regexp;
      }

      return self._pattern;
    };

    this.reset = function(executeCallbacks) {

      self._cancelStartTimeout();
      state.blockCount = 0;
      state.blocking = false;

      // Restore the focus to the element that was active
      // before the block start, but not if the user has
      // focused something else while the block was active.

      if(self._restoreFocus &&
         (!$document[0].activeElement || $document[0].activeElement === $body[0])) {
        self._restoreFocus.focus();
        self._restoreFocus = null;
      }

      try {
        if (executeCallbacks) {
          angular.forEach(doneCallbacks, function(cb) {
            cb();
          });
        }
      } finally {
        doneCallbacks.length = 0;
      }
    };

    this.done = function(fn) {
      doneCallbacks.push(fn);
    };

    this.state = function() {
      return state;
    };

    this.addRef = function() {
      self._refs += 1;
    };

    this.release = function() {
      if(--self._refs <= 0) {
        mainBlock.instances._destroy(self);
      }
    };
  }

  var instances = [];

  instances.get = function(id) {

    if(!isNaN(id)) {
      throw new Error('BlockUI id cannot be a number');
    }

    var instance = instances[id];

    if(!instance) {
      // TODO: ensure no array instance trashing [xxx] -- current workaround: '_' + $scope.$id
      instance = instances[id] = new BlockUI(id);
      instances.push(instance);
    }

    return instance;
  };

  instances._destroy = function(idOrInstance) {
    if (angular.isString(idOrInstance)) {
      idOrInstance = instances[idOrInstance];
    }

    if (idOrInstance) {
      idOrInstance.reset();

      var i = blockUIUtils.indexOf(instances, idOrInstance);
      instances.splice(i, 1);

      delete instances[idOrInstance.state().id];
    }
  };

  instances.locate = function(request) {

    var result = [];

    // Add function wrappers that will be executed on every item
    // in the array.

    blockUIUtils.forEachFnHook(result, 'start');
    blockUIUtils.forEachFnHook(result, 'stop');

    var i = instances.length;

    while(i--) {
      var instance = instances[i];
      var pattern = instance._pattern;

      if(pattern && pattern.test(request.url)) {
        result.push(instance);
      }
    }

    if(result.length === 0) {
      result.push(mainBlock);
    }

    return result;
  };

  // Propagate the reset to all instances

  blockUIUtils.forEachFnHook(instances, 'reset');

  var mainBlock = instances.get('main');

  mainBlock.addRef();
  mainBlock.instances = instances;

  return mainBlock;
});
<commit_msg>fix zero delay & ie restore invisible focus<commit_after>blkUI.factory('blockUI', function(blockUIConfig, $timeout, blockUIUtils, $document) {

  var $body = $document.find('body');

  function BlockUI(id) {

    var self = this;

    var state = {
      id: id,
      blockCount: 0,
      message: blockUIConfig.message,
      blocking: false
    }, startPromise, doneCallbacks = [];

    this._id = id;

    this._refs = 0;

    this.start = function(message) {

      if(state.blockCount > 0) {
        message = message || state.message || blockUIConfig.message;
      } else {
        message = message || blockUIConfig.message;
      }

      state.message = message;

      state.blockCount++;

      // Check if the focused element is part of the block scope

      var $ae = angular.element($document[0].activeElement);

      if($ae.length && blockUIUtils.isElementInBlockScope($ae, self)) {

        // Let the active element lose focus and store a reference
        // to restore focus when we're done (reset)

        self._restoreFocus = $ae[0];

        // https://github.com/McNull/angular-block-ui/issues/13
        // http://stackoverflow.com/questions/22698058/apply-already-in-progress-error-when-using-typeahead-plugin-found-to-be-relate
        // Queue the blur after any ng-blur expression.

        $timeout(function() {
          // Ensure we still need to blur
          // Don't restore if active element is body, since this causes IE to switch windows (see http://tjvantoll.com/2013/08/30/bugs-with-document-activeelement-in-internet-explorer/)
          if (self._restoreFocus && self._restoreFocus !== $body[0]) {
            self._restoreFocus.blur();
          }
        });
      }

      if (!startPromise && blockUIConfig.delay !== 0) {
        startPromise = $timeout(block, blockUIConfig.delay);
      } else if (blockUIConfig.delay === 0) {
        block();
      }

      function block () {
        startPromise = null;
        state.blocking = true;
      }
    };

    this._cancelStartTimeout = function() {
      if (startPromise) {
        $timeout.cancel(startPromise);
        startPromise = null;
      }
    };

    this.stop = function() {
      state.blockCount = Math.max(0, --state.blockCount);

      if (state.blockCount === 0) {
        self.reset(true);
      }
    };

    this.isBlocking = function () {
        return state.blocking;
    };

    this.message = function(value) {
      state.message = value;
    };

    this.pattern = function(regexp) {
      if (regexp !== undefined) {
        self._pattern = regexp;
      }

      return self._pattern;
    };

    this.reset = function(executeCallbacks) {

      self._cancelStartTimeout();
      state.blockCount = 0;
      state.blocking = false;

      // Restore the focus to the element that was active
      // before the block start, but not if the user has
      // focused something else while the block was active.

      if(self._restoreFocus &&
         (!$document[0].activeElement || $document[0].activeElement === $body[0])) {

        //IE8 will throw if element for setting focus is invisible
        try {
          self._restoreFocus.focus();
        } catch(e1) {
          (function () {
              var elementToFocus = self._restoreFocus;
              $timeout(function() {
                if(elementToFocus) {
                  try {
                    elementToFocus.focus();
                  } catch(e2) { }
              }
            },100);
          })();
        }

        self._restoreFocus = null;
      }

      try {
        if (executeCallbacks) {
          angular.forEach(doneCallbacks, function(cb) {
            cb();
          });
        }
      } finally {
        doneCallbacks.length = 0;
      }
    };

    this.done = function(fn) {
      doneCallbacks.push(fn);
    };

    this.state = function() {
      return state;
    };

    this.addRef = function() {
      self._refs += 1;
    };

    this.release = function() {
      if(--self._refs <= 0) {
        mainBlock.instances._destroy(self);
      }
    };
  }

  var instances = [];

  instances.get = function(id) {

    if(!isNaN(id)) {
      throw new Error('BlockUI id cannot be a number');
    }

    var instance = instances[id];

    if(!instance) {
      // TODO: ensure no array instance trashing [xxx] -- current workaround: '_' + $scope.$id
      instance = instances[id] = new BlockUI(id);
      instances.push(instance);
    }

    return instance;
  };

  instances._destroy = function(idOrInstance) {
    if (angular.isString(idOrInstance)) {
      idOrInstance = instances[idOrInstance];
    }

    if (idOrInstance) {
      idOrInstance.reset();

      var i = blockUIUtils.indexOf(instances, idOrInstance);
      instances.splice(i, 1);

      delete instances[idOrInstance.state().id];
    }
  };

  instances.locate = function(request) {

    var result = [];

    // Add function wrappers that will be executed on every item
    // in the array.

    blockUIUtils.forEachFnHook(result, 'start');
    blockUIUtils.forEachFnHook(result, 'stop');

    var i = instances.length;

    while(i--) {
      var instance = instances[i];
      var pattern = instance._pattern;

      if(pattern && pattern.test(request.url)) {
        result.push(instance);
      }
    }

    if(result.length === 0) {
      result.push(mainBlock);
    }

    return result;
  };

  // Propagate the reset to all instances

  blockUIUtils.forEachFnHook(instances, 'reset');

  var mainBlock = instances.get('main');

  mainBlock.addRef();
  mainBlock.instances = instances;

  return mainBlock;
});
<|endoftext|>


<commit_before>/* global angular, console */

angular.module('angular-google-analytics', [])
    .provider('Analytics', function() {
        'use strict';
        var created = false,
            trackRoutes = true,
            accountId,
            trackPrefix = '',
            domainName,
            trackEcommerce = false,
            ecommerceLoaded = false;

        this._logs = [];

        // config methods
        this.setAccount = function(id) {
          accountId = id;
          return true;
        };
        this.trackPages = function(doTrack) {
          trackRoutes = doTrack;
          return true;
        };
        this.trackPrefix = function(prefix) {
          trackPrefix = prefix;
          return true;
        };

        this.setDomainName = function(domain) {
          domainName = domain;
          return true;
        };

        this.trackEcommerce = function(doTrack) {
          trackEcommerce = doTrack;
          return true;
        };

        // public service
        this.$get = ['$document', '$rootScope', '$location', '$window', function($document, $rootScope, $location, $window) {
          // private methods
          function _createScriptTag() {
            //require accountId
            if (!accountId) return;

            //initialize the window object __gaTracker
            $window.GoogleAnalyticsObject = '__gaTracker';
            if(angular.isUndefined($window.__gaTracker)) {
                $window.__gaTracker = function() {
                    if(angular.isUndefined($window.__gaTracker.q)) {
                        $window.__gaTracker.q = [];
                    }
                    $window.__gaTracker.q.push(arguments);
                };
                $window.__gaTracker.l=1*new Date();
            }
            var opts = {};
            if(domainName) {
                opts.cookieDomain = domainName;
            }

            $window.__gaTracker('create', accountId);

            if (trackEcommerce && !ecommerceLoaded) {
                $window.__gaTracker('require', 'ecommerce', 'ecommerce.js');
                ecommerceLoaded = true;
            }

            if (trackRoutes) $window.__gaTracker('send','pageview');

            // inject the google analytics tag
            (function() {
              var gaTag = $document[0].createElement('script');
              gaTag.type = 'text/javascript';
              gaTag.async = true;
              gaTag.src = '//www.google-analytics.com/analytics.js';
              var s = $document[0].getElementsByTagName('script')[0];
              s.parentNode.insertBefore(gaTag, s);
            })();
            created = true;
          }
          // for testing
          this._log = function() {
            this._logs.push(arguments);
          };

          /**
            * Track Pageview
            * https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#hit
            * @param url (optional, if not specified $location.path() will be used, and this gets the trackPrefix prefixed onto it)
            * @param title (optional, if not specified $rootScope.pageTitle will be used if defined, else let GA use the document.title property)
            * @private
            */
          this._trackPage = function(url,title) {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              if (angular.isUndefined(url)) { url = $location.path(); }
              var fullUrl = trackPrefix + url;
              if (fullUrl != '' && fullUrl.charAt(0) !== '/') { fullUrl = '/' + fullUrl; } //page should always start with a /
              var opts = { 'page': fullUrl };

              if(angular.isUndefined(title) && angular.isDefined($rootScope.pageTitle)) { title = $rootScope.pageTitle; }
              if(angular.isDefined(title) && title !== '') { opts.title = title; }

              $window.__gaTracker('send','pageview', opts);
              this._log('pageview', arguments);
          };

          /**
            * Track Event
            * https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#events
            * @param category
            * @param action
            * @param label
            * @param value
            * @private
            */
          this._trackEvent = function(category, action, label, value) {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('send','event', {
                  'eventCategory': category,
                  'eventAction': action,
                  'eventLabel': label,
                  'eventValue': value
              });
              this._log('event', arguments);
          };

          /**
           * Add transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#addTrans
           * @param transactionId
           * @param affiliation
           * @param total
           * @param tax
           * @param shipping
           * @private
           * temporarily removing city, state, country as I don't see these in the newer documentation
           */
          this._addTrans = function (transactionId, affiliation, total, tax, shipping) {
            if (angular.isUndefined($window.__gaTracker)) { return; }

            //guard in case ecommerce hasn't been loaded, shouldn't really happen
            if(trackEcommerce && !ecommerceLoaded) {
                $window.__gaTracker('require', 'ecommerce', 'ecommerce.js');
                ecommerceLoaded = true;
            }

            $window.__gaTracker('ecommerce:addTransaction', {
                'id': transactionId,
                'affiliation': affiliation,
                'revenue': total,
                'shipping': shipping,
                'tax': tax
                //'city': city,
                //'state': state,
                //'country': country
            });
            this._log('ecommerce:addTransaction', arguments);
          };

          /**
           * Add item to transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#addItem
           * @param transactionId
           * @param name
           * @param sku
           * @param category
           * @param price
           * @param quantity
           * @private
           */
          this._addItem = function (transactionId, name, sku, category, price, quantity) {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('ecommerce:addItem', {
                  'id': transactionId,
                  'name': name,
                  'sku': sku,
                  'category': category,
                  'price': price,
                  'quantity': quantity
              });
              this._log('ecommerce:addItem', arguments);
          };

          /**
           * Track transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#sendingData
           * @private
           */
          this._trackTrans = function () {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('ecommerce:send');
              this._log('ecommerce:send', arguments);
          };

          /**
           * Clear transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#clearingData
           * @private
           */
          this._clearTrans = function () {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('ecommerce:clear');
              this._log('ecommerce:clear', arguments);
          };

          // --------- initialization steps -----------------------
          // creates the ganalytics tracker
          _createScriptTag();

          var me = this;

          // activates page tracking
          if (trackRoutes) {
              $rootScope.$on('$routeChangeSuccess', function() {
                  me._trackPage($location.path(), $rootScope.pageTitle);
              });
          }
          // --------- end initialization steps -----------------------

          // the rest of the public interface
          return {
                _logs: me._logs,
                trackPage: function(url, title) {
                    // add a page event
                    me._trackPage(url, title);
                },
                trackEvent: function(category, action, label, value) {
                    // add an action event
                    me._trackEvent(category, action, label, value);
                },
                addTrans: function (transactionId, affiliation, total, tax, shipping) {
                    me._addTrans(transactionId, affiliation, total, tax, shipping);
                },
                addItem: function (transactionId, sku, name, category, price, quantity) {
                    me._addItem(transactionId, name, sku, category, price, quantity);
                },
                trackTrans: function () {
                    me._trackTrans();
                },
                clearTrans: function () {
                    me._clearTrans();
                },
                ga: function() {
                    if(angular.isDefined($window.__gaTracker)) {
                        $window.__gaTracker(arguments);
                    }
                }
            };
        }];

    });
<commit_msg>Not sure the ga function is working, so I added explicit trackSocial function.<commit_after>/* global angular, console */

angular.module('angular-google-analytics', [])
    .provider('Analytics', function() {
        'use strict';
        var created = false,
            trackRoutes = true,
            accountId,
            trackPrefix = '',
            domainName,
            trackEcommerce = false,
            ecommerceLoaded = false;

        this._logs = [];

        // config methods
        this.setAccount = function(id) {
          accountId = id;
          return true;
        };
        this.trackPages = function(doTrack) {
          trackRoutes = doTrack;
          return true;
        };
        this.trackPrefix = function(prefix) {
          trackPrefix = prefix;
          return true;
        };

        this.setDomainName = function(domain) {
          domainName = domain;
          return true;
        };

        this.trackEcommerce = function(doTrack) {
          trackEcommerce = doTrack;
          return true;
        };

        // public service
        this.$get = ['$document', '$rootScope', '$location', '$window', function($document, $rootScope, $location, $window) {
          // private methods
          function _createScriptTag() {
            //require accountId
            if (!accountId) return;

            //initialize the window object __gaTracker
            $window.GoogleAnalyticsObject = '__gaTracker';
            if(angular.isUndefined($window.__gaTracker)) {
                $window.__gaTracker = function() {
                    if(angular.isUndefined($window.__gaTracker.q)) {
                        $window.__gaTracker.q = [];
                    }
                    $window.__gaTracker.q.push(arguments);
                };
                $window.__gaTracker.l=1*new Date();
            }
            var opts = {};
            if(domainName) {
                opts.cookieDomain = domainName;
            }

            $window.__gaTracker('create', accountId);

            if (trackEcommerce && !ecommerceLoaded) {
                $window.__gaTracker('require', 'ecommerce', 'ecommerce.js');
                ecommerceLoaded = true;
            }

            if (trackRoutes) $window.__gaTracker('send','pageview');

            // inject the google analytics tag
            (function() {
              var gaTag = $document[0].createElement('script');
              gaTag.type = 'text/javascript';
              gaTag.async = true;
              gaTag.src = '//www.google-analytics.com/analytics.js';
              var s = $document[0].getElementsByTagName('script')[0];
              s.parentNode.insertBefore(gaTag, s);
            })();
            created = true;
          }
          // for testing
          this._log = function() {
            this._logs.push(arguments);
          };

          /**
            * Track Pageview
            * https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#hit
            * @param url (optional, if not specified $location.path() will be used, and this gets the trackPrefix prefixed onto it)
            * @param title (optional, if not specified $rootScope.pageTitle will be used if defined, else let GA use the document.title property)
            * @private
            */
          this._trackPage = function(url,title) {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              if (angular.isUndefined(url)) { url = $location.path(); }
              var fullUrl = trackPrefix + url;
              if (fullUrl != '' && fullUrl.charAt(0) !== '/') { fullUrl = '/' + fullUrl; } //page should always start with a /
              var opts = { 'page': fullUrl };

              if(angular.isUndefined(title) && angular.isDefined($rootScope.pageTitle)) { title = $rootScope.pageTitle; }
              if(angular.isDefined(title) && title !== '') { opts.title = title; }

              $window.__gaTracker('send','pageview', opts);
              this._log('pageview', arguments);
          };

          /**
            * Track Event
            * https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#events
            * @param category
            * @param action
            * @param label
            * @param value
            * @private
            */
          this._trackEvent = function(category, action, label, value) {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('send','event', {
                  'eventCategory': category,
                  'eventAction': action,
                  'eventLabel': label,
                  'eventValue': value
              });
              this._log('event', arguments);
          };

          /**
           * Add transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#addTrans
           * @param transactionId
           * @param affiliation
           * @param total
           * @param tax
           * @param shipping
           * @private
           * temporarily removing city, state, country as I don't see these in the newer documentation
           */
          this._addTrans = function (transactionId, affiliation, total, tax, shipping) {
            if (angular.isUndefined($window.__gaTracker)) { return; }

            //guard in case ecommerce hasn't been loaded, shouldn't really happen
            if(trackEcommerce && !ecommerceLoaded) {
                $window.__gaTracker('require', 'ecommerce', 'ecommerce.js');
                ecommerceLoaded = true;
            }

            $window.__gaTracker('ecommerce:addTransaction', {
                'id': transactionId,
                'affiliation': affiliation,
                'revenue': total,
                'shipping': shipping,
                'tax': tax
                //'city': city,
                //'state': state,
                //'country': country
            });
            this._log('ecommerce:addTransaction', arguments);
          };

          /**
           * Add item to transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#addItem
           * @param transactionId
           * @param name
           * @param sku
           * @param category
           * @param price
           * @param quantity
           * @private
           */
          this._addItem = function (transactionId, name, sku, category, price, quantity) {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('ecommerce:addItem', {
                  'id': transactionId,
                  'name': name,
                  'sku': sku,
                  'category': category,
                  'price': price,
                  'quantity': quantity
              });
              this._log('ecommerce:addItem', arguments);
          };

          /**
           * Track transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#sendingData
           * @private
           */
          this._trackTrans = function () {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('ecommerce:send');
              this._log('ecommerce:send', arguments);
          };

          /**
           * Clear transaction
           * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#clearingData
           * @private
           */
          this._clearTrans = function () {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('ecommerce:clear');
              this._log('ecommerce:clear', arguments);
          };

          this._trackSocial = function(network, action, target) {
              if (angular.isUndefined($window.__gaTracker)) { return; }

              $window.__gaTracker('send','social', {
                 'socialNetwork': network,
                 'socialAction': action,
                 'socialTarget': target
              });
              this._log('trackSocial', arguments);
          };

          // --------- initialization steps -----------------------
          // creates the ganalytics tracker
          _createScriptTag();

          var me = this;

          // activates page tracking
          if (trackRoutes) {
              $rootScope.$on('$routeChangeSuccess', function() {
                  me._trackPage($location.path(), $rootScope.pageTitle);
              });
          }
          // --------- end initialization steps -----------------------

          // the rest of the public interface
          return {
                _logs: me._logs,
                trackPage: function(url, title) {
                    // add a page event
                    me._trackPage(url, title);
                },
                trackEvent: function(category, action, label, value) {
                    // add an action event
                    me._trackEvent(category, action, label, value);
                },
                addTrans: function (transactionId, affiliation, total, tax, shipping) {
                    me._addTrans(transactionId, affiliation, total, tax, shipping);
                },
                addItem: function (transactionId, sku, name, category, price, quantity) {
                    me._addItem(transactionId, name, sku, category, price, quantity);
                },
                trackTrans: function () {
                    me._trackTrans();
                },
                clearTrans: function () {
                    me._clearTrans();
                },
                trackSocial: function(network, action, target) {
                    me._trackSocial(network, action, target);
                },
                ga: function() {
                    if(angular.isDefined($window.__gaTracker)) {
                        $window.__gaTracker(arguments);
                    }
                }
            };
        }];

    });
<|endoftext|>


<commit_before>var logger = require('loglevel')
var ngOpbeat = require('./ngOpbeat')
var TransactionService = require('../transaction/transaction_service')

function ServiceContainer (config) {
  this.services = {logger: logger}
  logger.setLevel(config.logLevel, false)
  var transactionService = this.services.transactionService = new TransactionService(logger, {})

  if (typeof window.zone === 'undefined') {
    require('zone.js')
  }

  var ZoneService = require('../transaction/zone_service')
  var zoneService = this.services.zoneService = new ZoneService(window.zone, transactionService, logger)

  // binding bootstrap to zone

  window.angular.bootstrap = zoneService.zone.bind(window.angular.bootstrap)

  window.name = 'NG_DEFER_BOOTSTRAP!' + window.name
  window.angular.resumeDeferredBootstrap = function () {
    var resumeBootstrap = zoneService.zone.bind(window.angular.resumeBootstrap)
    resumeBootstrap()
  }

  ngOpbeat(transactionService, logger)
}

module.exports = ServiceContainer
<commit_msg>removing zone.js deprecation warning fixes #50<commit_after>var logger = require('loglevel')
var ngOpbeat = require('./ngOpbeat')
var TransactionService = require('../transaction/transaction_service')

function ServiceContainer (config) {
  this.services = {logger: logger}
  logger.setLevel(config.logLevel, false)
  var transactionService = this.services.transactionService = new TransactionService(logger, {})

  if (typeof window.zone === 'undefined') {
    require('zone.js')
  }

  function noop () {}
  var zonePrototype = ('getPrototypeOf' in Object)
    ? Object.getPrototypeOf(window.zone) : window.zone.__proto__ // eslint-disable-line

  zonePrototype.enqueueTask = noop
  zonePrototype.dequeueTask = noop

  var ZoneService = require('../transaction/zone_service')
  var zoneService = this.services.zoneService = new ZoneService(window.zone, transactionService, logger)

  // binding bootstrap to zone

  window.angular.bootstrap = zoneService.zone.bind(window.angular.bootstrap)

  window.name = 'NG_DEFER_BOOTSTRAP!' + window.name
  window.angular.resumeDeferredBootstrap = function () {
    var resumeBootstrap = zoneService.zone.bind(window.angular.resumeBootstrap)
    resumeBootstrap()
  }

  ngOpbeat(transactionService, logger)
}

module.exports = ServiceContainer
<|endoftext|>


<commit_before>import textfield from './textfield.css';

/** @jsx h */
import { h } from 'hyperapp';
import Ripple from './Ripple'; // eslint-disable-line no-unused-vars
import randomString from '../../utils/random-string';

const TextField = ({
  id,
  key,
  value,
  type = 'text',
  placeholder,
  required,
  disabled,
  label,
  floatingLabel,
  style,
  ripple,
  ...otherProps }) => {

  const create = element => {
    const input = element.querySelector('input');
    const label = element.querySelector('label');

    if (label) {
      input.id = id || `textfield-${randomString()}`;
      label.setAttribute('for', input.id);

      if (floatingLabel) {
        if(input !== document.activeElement && input.value.length < 1) {
          label.classList.add(textfield['TextField__label--inline']);
        }

        input.addEventListener('focus', () => {
          label.classList.remove(textfield['TextField__label--inline']);
        });

        input.addEventListener('focusout', (event) => {
          if (event.target.value.length < 1) {
            label.classList.add(textfield['TextField__label--inline']);
          }
        });

        label.addEventListener('mousedown', (event) => {
          if (!label.classList.contains(textfield['TextField__label--inline']) && document.activeElement === input) {
            event.preventDefault();
            input.focus();
          }
        });
      }
    }
    else if (id) {
      input.id = id;
    }

    element.addEventListener('click', (event) => {
      if(event.target === element) {
        input.focus();
      }
    });
  };

  return (

 create(element)}
    >
      <input
        class={textfield.TextField__input}
        type={type}
        value={value}
        disabled={disabled}
        required={required}
        placeholder={placeholder}
        {...otherProps}
      />
      {label}

      {ripple && <Ripple/>}

  );
};

export default TextField;

<commit_msg>Added mousedown event listener to label<commit_after>import textfield from './textfield.css';

/** @jsx h */
import { h } from 'hyperapp';
import Ripple from './Ripple'; // eslint-disable-line no-unused-vars
import randomString from '../../utils/random-string';

const TextField = ({
  id,
  key,
  value,
  type = 'text',
  placeholder,
  required,
  disabled,
  label,
  floatingLabel,
  style,
  ripple,
  ...otherProps }) => {

  const create = element => {
    const input = element.querySelector('input');
    const label = element.querySelector('label');

    if (label) {
      input.id = id || `textfield-${randomString()}`;
      label.setAttribute('for', input.id);

      if (floatingLabel) {
        if(input !== document.activeElement && input.value.length < 1) {
          label.classList.add(textfield['TextField__label--inline']);
        }

        input.addEventListener('focus', () => {
          label.classList.remove(textfield['TextField__label--inline']);
        });

        input.addEventListener('focusout', (event) => {
          if (event.target.value.length < 1) {
            label.classList.add(textfield['TextField__label--inline']);
          }
        });
      }

      label.addEventListener('mousedown', (event) => {
        if (document.activeElement === input &&
          !label.classList.contains(textfield['TextField__label--inline'])) {

          event.preventDefault();
          input.focus();
        }
      });
    }
    else if (id) {
      input.id = id;
    }

    element.addEventListener('click', (event) => {
      if(event.target === element) {
        input.focus();
      }
    });
  };

  return (

 create(element)}
    >
      <input
        class={textfield.TextField__input}
        type={type}
        value={value}
        disabled={disabled}
        required={required}
        placeholder={placeholder}
        {...otherProps}
      />
      {label}

      {ripple && <Ripple/>}

  );
};

export default TextField;

<|endoftext|>


<commit_before>(function() {
	'use strict';

	angular
		.module('inclusive')
		.controller('MainController', MainController);

	/** @ngInject */
	function MainController($mdToast) {

		var vm = this;

		vm.model = {

			phrase: ''

		};

		vm.makeInclusive = makeInclusive;
		vm.inclusiveCopySuccess = inclusiveCopySuccess

		initialize();

		function initialize () {

			vm.inclusiveResult = makeLanguageInclusive(vm.model.phrase);

		}

		function makeInclusive () {

			vm.inclusiveResult = makeLanguageInclusive(vm.model.phrase);

		}

		function inclusiveCopySuccess (e) {

			e.clearSelection();

			$mdToast
				.show(
					$mdToast.simple()
						.textContent('Ya puedes pegar tu frase y hacer el mundo inclusivo nuevamente')
						.position('top right')
						.hideDelay(5000)
				);

		}

		function makeLanguageInclusive(str) {

			var result = {};

			if (str.length > 0) {

				var regExp = /[aeiouáéíóúü]/gi;

				result = {

					totalLetters: str.length,

					totalNoInclusive: str.match(regExp).length,

					inclusivePhrase: str.replace(regExp, 'x')

				};

				result.unfairPhrase = (result.totalNoInclusive / result.totalLetters) * 100;

			}

			return result;

		}

	}

})();
<commit_msg>Fixed inclusive length words<commit_after>(function() {
	'use strict';

	angular
		.module('inclusive')
		.controller('MainController', MainController);

	/** @ngInject */
	function MainController($mdToast) {

		var vm = this;

		vm.model = {

			phrase: ''

		};

		vm.makeInclusive = makeInclusive;
		vm.inclusiveCopySuccess = inclusiveCopySuccess

		initialize();

		function initialize () {

			vm.inclusiveResult = makeLanguageInclusive(vm.model.phrase);

		}

		function makeInclusive () {

			vm.inclusiveResult = makeLanguageInclusive(vm.model.phrase);

		}

		function inclusiveCopySuccess (e) {

			e.clearSelection();

			$mdToast
				.show(
					$mdToast.simple()
						.textContent('Ya puedes pegar tu frase y hacer el mundo inclusivo nuevamente')
						.position('top right')
						.hideDelay(5000)
				);

		}

		function makeLanguageInclusive(str) {

			var result = {};

			if (str.length > 0) {

				var regExp = /[aeiouáéíóúü]/gi;

				result = {

					totalLetters: str.replace(/ /g, '').length,

					totalNoInclusive: str.match(regExp).length,

					inclusivePhrase: str.replace(regExp, 'x')

				};

				result.unfairPhrase = (result.totalNoInclusive / result.totalLetters) * 100;

			}

			return result;

		}

	}

})();
<|endoftext|>


<commit_before>const map = {
    //steemit
    ned: 'steemit',
    justinw: 'steemit',
    elipowell: 'steemit',
    vandeberg: 'steemit',
    gerbino: 'steemit',
    andrarchy: 'steemit',
    roadscape: 'steemit',
    steemitblog: 'steemit',
    steemitdev: 'steemit',
    justinsunsteemit: 'steemit',

    /*
    //steem monsters
    steemmonsters: 'sm',
    'steem.monsters': 'sm',
    aggroed: 'sm',
    yabapmatt: 'sm',
*/
};

export default map;
<commit_msg>remove Ned from AffiliationMap.js<commit_after>const map = {
    //steemit
    justinw: 'steemit',
    elipowell: 'steemit',
    vandeberg: 'steemit',
    gerbino: 'steemit',
    andrarchy: 'steemit',
    roadscape: 'steemit',
    steemitblog: 'steemit',
    steemitdev: 'steemit',
    justinsunsteemit: 'steemit',

    /*
    //steem monsters
    steemmonsters: 'sm',
    'steem.monsters': 'sm',
    aggroed: 'sm',
    yabapmatt: 'sm',
*/
};

export default map;
<|endoftext|>


<commit_before>// Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details.
/*global Viz */

window.jdls = window.jdls || {};

// Functions for turning an object graph into SVG.
(function() {
	"use strict";

	var exports = window.jdls.viz = {};
	var details = exports.details = {};

	exports.render = function render(rootName, object, options) {
		return details.vizToSvg(details.graphToViz(new jdls.ObjectGraph(rootName, object, options)));
	};

	details.vizToSvg = function vizToSvg(vizCode) {
		/*jshint newcap:false */
		return Viz(vizCode, "svg");
	};

	details.graphToViz = function graphToViz(graph) {
		return '' +
			'digraph g {\n' +
			'  graph [\n' +
			'    rankdir = "LR"\n' +
			'  ];\n' +
			'  node [\n' +
			'    fontname = "Helvetica"\n' +
			'    fontsize = "10"\n' +
			'    shape = "plaintext"\n' +   // 'plaintext' is misnamed; it enables HTML-like formatting
			'  ];\n' +
			'  edge [];\n' +
			'  \n' +
			nodes() +
			edges() +
			'}\n';

		function nodes() {
			return graph.nodes().map(function(node) {
				return details.nodeToViz(node);
			}).join("");
		}

		function edges() {
			return graph.edges().map(function(edge) {
				return details.edgeToViz(edge);
			}).join("");
		}
	};

	details.nodeToViz = function nodeToViz(node) {

//			'label = "' + '<title>' + escape(node.title()) + fields() + '"\n' +
//			'shape = "record"' +
//			'];\n';

		return '' +
			'  "' + node.id() + '" [label=<\n' +
			'
\n' +
			'      ' + escapeHtml(node.title()) + '		\n' +
			fields() +
			prototype() +
			'
\n' +
			'  >];\n';

		function fields() {
			var oddRow = true;
			return node.properties().map(function(property) {
				var color = oddRow ? "#F0F0F0" : "white";// "#9ED3E5";
				oddRow = !oddRow;
				var result = '
' + escapeHtml(property.name) + ': ' + escapeHtml(property.value) + '
\n';
				return result;
			}).join("");
		}

		function prototype() {
			var proto = node.prototype();
			return '
' + escapeHtml(proto.name) + ': ' + escapeHtml(proto.value) + '
\n';
		}
	};

	details.edgeToViz = function edgeToViz(edge) {
		return '"' + edge.from.id() + '":' + edge.fromField + ' -> "' + edge.to.id() + '":title [];';
	};

	var escapeViz = details.escapeViz = function escapeViz(name) {
		return name.
			replace(/\\/g, '\\\\').
			replace(/\{/g, "\\{").
			replace(/\}/g, "\\}").
			replace(/</g, "\\<").
			replace(/>/g, "\\>").
			replace(/\|/g, '\\|').
			replace(/\"/g, '\\"').
			replace(/\n/g, '\\n').
			replace(/\t/g, ' ');
	};

	var escapeHtml = details.escapeHtml = function escapeHtml(html) {
		return html.
			replace(/&/g, "&").
			replace(/</g, "<").
			replace(/>/g, ">").
			replace(/"/g, """).
			replace(/'/g, "'").
			replace(/\n/g, '
').
			replace(/\t/g, '  ');
	};

}());
<commit_msg>Factored out graph styling constants<commit_after>// Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details.
/*global Viz */

window.jdls = window.jdls || {};

// Functions for turning an object graph into SVG.
(function() {
	"use strict";

	var exports = window.jdls.viz = {};
	var details = exports.details = {};

	var TABLE_FONT_POINTS = 10;

	var TITLE_BACKGROUND_COLOR = "#00668F";
	var TITLE_FONT_COLOR = "white";
	var TITLE_FONT_POINTS = TABLE_FONT_POINTS + 1;

	var PROPERTY_BACKGROUND_COLOR = "#F0F0F0";
	var PROPERTY_ALT_BACKGROUND_COLOR = "white";
	var PROPERTY_FONT_COLOR = "#999999";

	var PROTOTYPE_BACKGROUND_COLOR = "#0082B6";
	var PROTOTYPE_FONT_COLOR = "white";

	exports.render = function render(rootName, object, options) {
		return details.vizToSvg(details.graphToViz(new jdls.ObjectGraph(rootName, object, options)));
	};

	details.vizToSvg = function vizToSvg(vizCode) {
		/*jshint newcap:false */
		return Viz(vizCode, "svg");
	};

	details.graphToViz = function graphToViz(graph) {
		return '' +
			'digraph g {\n' +
			'  graph [\n' +
			'    rankdir = "LR"\n' +
			'  ];\n' +
			'  node [\n' +
			'    fontname = "Helvetica"\n' +
			'    fontsize = "' + TABLE_FONT_POINTS + '"\n' +
			'    shape = "plaintext"\n' +   // 'plaintext' is misnamed; it enables HTML-like formatting
			'  ];\n' +
			'  edge [];\n' +
			'  \n' +
			nodes() +
			edges() +
			'}\n';

		function nodes() {
			return graph.nodes().map(function(node) {
				return details.nodeToViz(node);
			}).join("");
		}

		function edges() {
			return graph.edges().map(function(edge) {
				return details.edgeToViz(edge);
			}).join("");
		}
	};

	details.nodeToViz = function nodeToViz(node) {

//			'label = "' + '<title>' + escape(node.title()) + fields() + '"\n' +
//			'shape = "record"' +
//			'];\n';

		return '' +
			'  "' + node.id() + '" [label=<\n' +
			'
\n' +
			'      ' + escapeHtml(node.title()) + '		\n' +
			fields() +
			prototype() +
			'
\n' +
			'  >];\n';

		function fields() {
			var oddRow = true;
			return node.properties().map(function(property) {
				var color = oddRow ? PROPERTY_BACKGROUND_COLOR : PROPERTY_ALT_BACKGROUND_COLOR;// "#9ED3E5";
				oddRow = !oddRow;
				var result = '
' + escapeHtml(property.name) + ': ' + escapeHtml(property.value) + '
\n';
				return result;
			}).join("");
		}

		function prototype() {
			var proto = node.prototype();
			return '
' + escapeHtml(proto.name) + ': ' + escapeHtml(proto.value) + '
\n';
		}
	};

	details.edgeToViz = function edgeToViz(edge) {
		return '"' + edge.from.id() + '":' + edge.fromField + ' -> "' + edge.to.id() + '":title [];';
	};

	var escapeViz = details.escapeViz = function escapeViz(name) {
		return name.
			replace(/\\/g, '\\\\').
			replace(/\{/g, "\\{").
			replace(/\}/g, "\\}").
			replace(/</g, "\\<").
			replace(/>/g, "\\>").
			replace(/\|/g, '\\|').
			replace(/\"/g, '\\"').
			replace(/\n/g, '\\n').
			replace(/\t/g, ' ');
	};

	var escapeHtml = details.escapeHtml = function escapeHtml(html) {
		return html.
			replace(/&/g, "&").
			replace(/</g, "<").
			replace(/>/g, ">").
			replace(/"/g, """).
			replace(/'/g, "'").
			replace(/\n/g, '
').
			replace(/\t/g, '  ');
	};

}());
<|endoftext|>


<commit_before>(function() {
  var numIntervals = 0,
    intervals = {};

  window.setCorrectingInterval = function (func, delay, minDelay /* optional */) {
    var id = numIntervals++,
      planned = Date.now() + delay;

    minDelay = minDelay || 0;

    function tick () {
      func();
      if (intervals[id]) {
        planned += delay;
        intervals[id] = setTimeout(tick, Math.max(planned - Date.now(), minDelay));
      }
    }

    intervals[id] = setTimeout(tick, delay);
    return id;
  };

  window.clearCorrectingInterval = function (id) {
    clearTimeout(intervals[id]);
    delete intervals[id];
  };
})();<commit_msg>Re-implement UMD pattern<commit_after>;(function(global, factory) {
  // Use UMD pattern to expose exported functions
  if (typeof exports === 'object') {
    // Expose to Node.js
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    // Expose to RequireJS
    define([], factory);
  }

  // Expose to global object (likely browser window)
  var exports = factory();
  for (var prop in exports) {
    global[prop] = exports[prop];
  }
}(this, function() {
  var numIntervals = 0,
    intervals = {};

  var setCorrectingInterval = function (func, delay, minDelay /* optional */) {
    var id = numIntervals++,
      planned = Date.now() + delay;

    minDelay = minDelay || 0;

    function tick () {
      func();
      if (intervals[id]) {
        planned += delay;
        intervals[id] = setTimeout(tick, Math.max(planned - Date.now(), minDelay));
      }
    }

    intervals[id] = setTimeout(tick, delay);
    return id;
  };

  var clearCorrectingInterval = function (id) {
    clearTimeout(intervals[id]);
    delete intervals[id];
  };

  return {
    setCorrectingInterval: setCorrectingInterval,
    clearCorrectingInterval: clearCorrectingInterval
  };
}));<|endoftext|>


<commit_before>import * as mats from './matrices';
import { intMatrices, matrices, residueClassRing } from './types';

const iops = intMatrices;
const fops = matrices;

const p = 9999991;
const pops = mats.extend(residueClassRing(p), ['Integer'], true);

const invModP = M => {
  const n = M.length;
  const A = M.map((row, i) => row.concat(pops.unitVector(n, i)));
  const E = pops.rowEchelonForm(A);

  if (fops.eq(E.map(row => row.slice(0, n)), fops.identityMatrix(n)))
    return E.map(row => row.slice(n));
};

const numberOfPAdicStepsNeeded = (A, b) => {
  const lengths = M => fops.transposed(M).map(r => fops.norm(r));
  const max = v => v.reduce((x, y) => x > y ? x : y);
  const product = v => v.reduce((x, y) => x * y);

  const ls = lengths(A).concat(max(lengths(b)));
  const delta = product(ls.sort().slice(A[0].length));
  const golden = (1 + Math.sqrt(5)) / 2;

  return Math.ceil(2 * Math.log(delta * golden) / Math.log(p));
};

const rationalReconstruction = (s, h) => {
  let u = [h, s];
  let v = [0, 1];
  let sign = 1;

  while (iops.gt(iops.times(u[1], u[1]), h)) {
    const q = iops.idiv(u[0], u[1]);

    u = [u[1], iops.minus(u[0], iops.times(q, u[1]))];
    v = [v[1], iops.plus(v[0], iops.times(q, v[1]))];
    sign *= -1;
  }

  return fops.div(iops.times(sign, u[1]), v[1]);
};

export default function solve(A, b) {
  const C = invModP(A);
  if (C == null)
    return null;

  const nrSteps = numberOfPAdicStepsNeeded(A, b);

  let bi = b;
  let pi = 1;
  let si = 0;

  for (let i = 0; i < nrSteps; ++i) {
    const xi = pops.times(C, bi);
    bi = iops.idiv(iops.minus(bi, iops.times(A, xi)), p);
    si = iops.plus(si, iops.times(pi, xi));
    pi = iops.times(pi, p);
  }

  return si.map(row => row.map(x => rationalReconstruction(x, pi)));
};

if (require.main == module) {
  Array.prototype.toString = function() {
    return '[ ' + this.map(x => x.toString()).join(', ') + ' ]';
  };

  const testSolver = (A, b) => {
    console.log();
    console.log(`solving ${A} * x = ${b}`);
    const x = solve(A, b);
    console.log(`  x = ${x}`);
    console.log(`  A * x = ${fops.times(A, x)}`);
  };

  testSolver(
    [ [  4, -4 ],
      [  1,  0 ] ],
    [ [  1,  1,  1 ],
      [  0,  0,  0 ] ]
  );
}
<commit_msg>add a jsverify test for the modular solver<commit_after>import * as mats from './matrices';
import { intMatrices, matrices, residueClassRing } from './types';

const iops = intMatrices;
const fops = matrices;

const p = 9999991;
const pops = mats.extend(residueClassRing(p), ['Integer'], true);

const invModP = M => {
  const n = M.length;
  const A = M.map((row, i) => row.concat(pops.unitVector(n, i)));
  const E = pops.rowEchelonForm(A);

  if (fops.eq(E.map(row => row.slice(0, n)), fops.identityMatrix(n)))
    return E.map(row => row.slice(n));
};

const numberOfPAdicStepsNeeded = (A, b) => {
  const lengths = M => fops.transposed(M).map(r => fops.norm(r));
  const max = v => v.reduce((x, y) => x > y ? x : y);
  const product = v => v.reduce((x, y) => x * y);

  const ls = lengths(A).concat(max(lengths(b)));
  const delta = product(ls.sort().slice(A[0].length));
  const golden = (1 + Math.sqrt(5)) / 2;

  return Math.ceil(2 * Math.log(delta * golden) / Math.log(p));
};

const rationalReconstruction = (s, h) => {
  let u = [h, s];
  let v = [0, 1];
  let sign = 1;

  while (iops.gt(iops.times(u[1], u[1]), h)) {
    const q = iops.idiv(u[0], u[1]);

    u = [u[1], iops.minus(u[0], iops.times(q, u[1]))];
    v = [v[1], iops.plus(v[0], iops.times(q, v[1]))];
    sign *= -1;
  }

  return fops.div(iops.times(sign, u[1]), v[1]);
};

export default function solve(A, b) {
  const C = invModP(A);
  if (C == null)
    return null;

  const nrSteps = numberOfPAdicStepsNeeded(A, b);

  let bi = b;
  let pi = 1;
  let si = 0;

  for (let i = 0; i < nrSteps; ++i) {
    const xi = pops.times(C, bi);
    bi = iops.idiv(iops.minus(bi, iops.times(A, xi)), p);
    si = iops.plus(si, iops.times(pi, xi));
    pi = iops.times(pi, p);
  }

  return si.map(row => row.map(x => rationalReconstruction(x, pi)));
};

if (require.main == module) {
  Array.prototype.toString = function() {
    return '[ ' + this.map(x => x.toString()).join(', ') + ' ]';
  };

  const testSolver = (A, b) => {
    console.log();
    console.log(`solving ${A} * x = ${b}`);
    const x = solve(A, b);
    console.log(`  x = ${x}`);
    console.log(`  A * x = ${fops.times(A, x)}`);
  };

  testSolver(
    [ [  4, -4 ],
      [  1,  0 ] ],
    [ [  1,  1,  1 ],
      [  0,  0,  0 ] ]
  );

  console.log();

  const toRows = (m, A) => {
    const r = [];
    for (let k = 0; k < A.length; k += m)
      r.push(A.slice(k, k + m));
    return r;
  };

  const splitArray = M => {
    const m = Math.floor(Math.sqrt(M.length));

    const A = M.slice(0, m * m);

    const b = M.slice(m * m);
    while (b.length == 0 || b.length % m)
      b.push(0);

    return [toRows(m, A), toRows(b.length / m, b)];
  };

  var jsc = require("jsverify");
  var solveReturnsASolution = jsc.forall(
    "nearray nat",
    M => {
      const [A, b] = splitArray(M);
      let x;
      try {
        x = solve(A, b);
      } catch(e) {
        console.log(`A = ${A}, b = ${b} => ${e}`);
        return false;
      }

      if (x == null)
        return true;

      const Ax = fops.times(A, x);
      const ok = fops.eq(Ax, b);

      if (!ok)
        console.log(`A = ${A}, b = ${b}, x = ${x}, A * x = ${Ax}`);
      return ok;
    })

  jsc.check(solveReturnsASolution, { tests: 1000, size: 100 });
}
<|endoftext|>