<commit_before>import {Fragment} from "./fragment"

// ::- Instances of this class represent a match state of a node
// type's [content expression](#model.NodeSpec.content), and can be
// used to find out whether further content matches here, and whether
// a given position is a valid end of the node.
export class ContentMatch {
  constructor(validEnd) {
    // :: bool
    // True when this match state represents a valid end of the node.
    this.validEnd = validEnd
    this.next = []
    this.wrapCache = []
  }

  static parse(string, nodeTypes) {
    let stream = new TokenStream(string, nodeTypes)
    if (stream.next == null) return ContentMatch.empty
    let expr = parseExpr(stream)
    if (stream.next) stream.err("Unexpected trailing text")
    let match = dfa(nfa(expr))
    checkForDeadEnds(match, stream)
    return match
  }

  // :: (NodeType) → ?ContentMatch
  // Match a node type, returning a match after that node if
  // successful.
  matchType(type) {
    for (let i = 0; i < this.next.length; i += 2)
      if (this.next[i] == type) return this.next[i + 1]
    return null
  }

  // :: (Fragment, ?number, ?number) → ?ContentMatch
  // Try to match a fragment. Returns the resulting match when
  // successful.
  matchFragment(frag, start = 0, end = frag.childCount) {
    let cur = this
    for (let i = start; cur && i < end; i++)
      cur = cur.matchType(frag.child(i).type)
    return cur
  }

  get inlineContent() {
    let first = this.next[0]
    return first ? first.isInline : false
  }

  get defaultType() {
    return this.next[0]
  }

  compatible(other) {
    for (let i = 0; i < this.next.length; i += 2)
      for (let j = 0; j < other.next.length; j += 2)
        if (this.next[i] == other.next[j]) return true
    return false
  }

  // :: (Fragment, bool, ?number) → ?Fragment
  // Try to match the given fragment, and if that fails, see if it can
  // be made to match by inserting nodes in front of it. When
  // successful, return a fragment of inserted nodes (which may be
  // empty if nothing had to be inserted). When `toEnd` is true, only
  // return a fragment if the resulting match goes to the end of the
  // content expression.
  fillBefore(after, toEnd = false, startIndex = 0) {
    let seen = [this]
    function search(match, types) {
      let finished = match.matchFragment(after, startIndex)
      if (finished && (!toEnd || finished.validEnd))
        return Fragment.from(types.map(tp => tp.createAndFill()))

      for (let i = 0; i < match.next.length; i += 2) {
        let type = match.next[i], next = match.next[i + 1]
        if (!(type.isText || type.hasRequiredAttrs()) && seen.indexOf(next) == -1) {
          seen.push(next)
          let found = search(next, types.concat(type))
          if (found) return found
        }
      }
    }

    return search(this, [])
  }

  // :: (NodeType) → ?[NodeType]
  // Find a set of wrapping node types that would allow a node of the
  // given type to appear at this position. The result may be empty
  // (when it fits directly) and will be null when no such wrapping
  // exists.
  findWrapping(target) {
    for (let i = 0; i < this.wrapCache.length; i += 2)
      if (this.wrapCache[i] == target) return this.wrapCache[i + 1]
    let computed = this.computeWrapping(target)
    this.wrapCache.push(target, computed)
    return computed
  }

  computeWrapping(target) {
    let seen = Object.create(null), active = [{match: this, type: null, via: null}]
    while (active.length) {
      let current = active.shift(), match = current.match
      if (match.matchType(target)) {
        let result = []
        for (let obj = current; obj.type; obj = obj.via)
          result.push(obj.type)
        return result.reverse()
      }
      for (let i = 0; i < match.next.length; i += 2) {
        let type = match.next[i]
        if (!type.isLeaf && !(type.name in seen) && (!current.type || match.next[i + 1].validEnd)) {
          active.push({match: type.contentMatch, type, via: current})
          seen[type.name] = true
        }
      }
    }
  }

  // :: number
  // The number of outgoing edges this node has in the finite
  // automaton that describes the content expression.
  get edgeCount() {
    return this.next.length >> 1
  }

  // :: (number) → {type: NodeType, next: ContentMatch}
  // Get the _n_th outgoing edge from this node in the finite
  // automaton that describes the content expression.
  edge(n) {
    let i = n << 1
    if (i > this.next.length) throw new RangeError(`There's no ${n}th edge in this content match`)
    return {type: this.next[i], next: this.next[i + 1]}
  }

  toString() {
    let seen = []
    function scan(m) {
      seen.push(m)
      for (let i = 1; i < m.next.length; i += 2)
        if (seen.indexOf(m.next[i]) == -1) scan(m.next[i])
    }
    scan(this)
    return seen.map((m, i) => {
      let out = i + (m.validEnd ? "*" : " ") + " "
      for (let i = 0; i < m.next.length; i += 2)
        out += (i ? ", " : "") + m.next[i].name + "->" + seen.indexOf(m.next[i + 1])
      return out
    }).join("\n")
  }
}

ContentMatch.empty = new ContentMatch(true)

class TokenStream {
  constructor(string, nodeTypes) {
    this.string = string
    this.nodeTypes = nodeTypes
    this.inline = null
    this.pos = 0
    this.tokens = string.split(/\s*(?=\b|\W|$)/)
    if (this.tokens[this.tokens.length - 1] == "") this.tokens.pop()
    if (this.tokens[0] == "") this.tokens.unshift()
  }

  get next() { return this.tokens[this.pos] }

  eat(tok) { return this.next == tok && (this.pos++ || true) }

  err(str) { throw new SyntaxError(str + " (in content expression '" + this.string + "')") }
}

function parseExpr(stream) {
  let exprs = []
  do { exprs.push(parseExprSeq(stream)) }
  while (stream.eat("|"))
  return exprs.length == 1 ? exprs[0] : {type: "choice", exprs}
}

function parseExprSeq(stream) {
  let exprs = []
  do { exprs.push(parseExprSubscript(stream)) }
  while (stream.next && stream.next != ")" && stream.next != "|")
  return exprs.length == 1 ? exprs[0] : {type: "seq", exprs}
}

function parseExprSubscript(stream) {
  let expr = parseExprAtom(stream)
  for (;;) {
    if (stream.eat("+"))
      expr = {type: "plus", expr}
    else if (stream.eat("*"))
      expr = {type: "star", expr}
    else if (stream.eat("?"))
      expr = {type: "opt", expr}
    else if (stream.eat("{"))
      expr = parseExprRange(stream, expr)
    else break
  }
  return expr
}

function parseNum(stream) {
  if (/\D/.test(stream.next)) stream.err("Expected number, got '" + stream.next + "'")
  let result = Number(stream.next)
  stream.pos++
  return result
}

function parseExprRange(stream, expr) {
  let min = parseNum(stream), max = min
  if (stream.eat(",")) {
    if (stream.next != "}") max = parseNum(stream)
    else max = -1
  }
  if (!stream.eat("}")) stream.err("Unclosed braced range")
  return {type: "range", min, max, expr}
}

function resolveName(stream, name) {
  let types = stream.nodeTypes, type = types[name]
  if (type) return [type]
  let result = []
  for (let typeName in types) {
    let type = types[typeName]
    if (type.groups.indexOf(name) > -1) result.push(type)
  }
  if (result.length == 0) stream.err("No node type or group '" + name + "' found")
  return result
}

function parseExprAtom(stream) {
  if (stream.eat("(")) {
    let expr = parseExpr(stream)
    if (!stream.eat(")")) stream.err("Missing closing paren")
    return expr
  } else if (!/\W/.test(stream.next)) {
    let exprs = resolveName(stream, stream.next).map(type => {
      if (stream.inline == null) stream.inline = type.isInline
      else if (stream.inline != type.isInline) stream.err("Mixing inline and block content")
      return {type: "name", value: type}
    })
    stream.pos++
    return exprs.length == 1 ? exprs[0] : {type: "choice", exprs}
  } else {
    stream.err("Unexpected token '" + stream.next + "'")
  }
}

// The code below helps compile a regular-expression-like language
// into a deterministic finite automaton. For a good introduction to
// these concepts, see https://swtch.com/~rsc/regexp/regexp1.html

// : (Object) → [[{term: ?any, to: number}]]
// Construct an NFA from an expression as returned by the parser. The
// NFA is represented as an array of states, which are themselves
// arrays of edges, which are `{term, to}` objects. The first state is
// the entry state and the last node is the success state.
//
// Note that unlike typical NFAs, the edge ordering in this one is
// significant, in that it is used to contruct filler content when
// necessary.
function nfa(expr) {
  let nfa = [[]]
  connect(compile(expr, 0), node())
  return nfa

  function node() { return nfa.push([]) - 1 }
  function edge(from, to, term) {
    let edge = {term, to}
    nfa[from].push(edge)
    return edge
  }
  function connect(edges, to) { edges.forEach(edge => edge.to = to) }

  function compile(expr, from) {
    if (expr.type == "choice") {
      return expr.exprs.reduce((out, expr) => out.concat(compile(expr, from)), [])
    } else if (expr.type == "seq") {
      for (let i = 0;; i++) {
        let next = compile(expr.exprs[i], from)
        if (i == expr.exprs.length - 1) return next
        connect(next, from = node())
      }
    } else if (expr.type == "star") {
      let loop = node()
      edge(from, loop)
      connect(compile(expr.expr, loop), loop)
      return [edge(loop)]
    } else if (expr.type == "plus") {
      let loop = node()
      connect(compile(expr.expr, from), loop)
      connect(compile(expr.expr, loop), loop)
      return [edge(loop)]
    } else if (expr.type == "opt") {
      return [edge(from)].concat(compile(expr.expr, from))
    } else if (expr.type == "range") {
      let cur = from
      for (let i = 0; i < expr.min; i++) {
        let next = node()
        connect(compile(expr.expr, cur), next)
        cur = next
      }
      if (expr.max == -1) {
        connect(compile(expr.expr, cur), cur)
      } else {
        for (let i = expr.min; i < expr.max; i++) {
          let next = node()
          edge(cur, next)
          connect(compile(expr.expr, cur), next)
          cur = next
        }
      }
      return [edge(cur)]
    } else if (expr.type == "name") {
      return [edge(from, null, expr.value)]
    }
  }
}

function cmp(a, b) { return a - b }

function nullFrom(nfa, node) {
  let result = []
  scan(node)
  return result.sort(cmp)

  function scan(node) {
    result.push(node)
    for (let a = nfa[node], i = 0; i < a.length; i++) {
      let {term, to} = a[i]
      if (!term && result.indexOf(to) == -1) scan(to)
    }
  }
}

// : ([[{term: ?any, to: number}]]) → ContentMatch
// Compiles an NFA as produced by `nfa` into a DFA, modeled as a set
// of state objects (`ContentMatch` instances) with transitions
// between them.
function dfa(nfa) {
  let labeled = Object.create(null)
  return explore(nullFrom(nfa, 0))

  function explore(states) {
    let out = []
    states.forEach(node => {
      nfa[node].forEach(({term, to}) => {
        if (!term) return
        let known = out.indexOf(term), set = known > -1 && out[known + 1]
        nullFrom(nfa, to).forEach(node => {
          if (!set) out.push(term, set = [])
          if (set.indexOf(node) == -1) set.push(node)
        })
      })
    })
    let state = labeled[states.join(",")] = new ContentMatch(states.indexOf(nfa.length - 1) > -1)
    for (let i = 0; i < out.length; i += 2) {
      let states = out[i + 1].sort(cmp)
      state.next.push(out[i], labeled[states.join(",")] || explore(states))
    }
    return state
  }
}

function checkForDeadEnds(match, stream) {
  for (let i = 0, work = [match]; i < work.length; i++) {
    let state = work[i], dead = !state.validEnd, nodes = []
    for (let j = 0; j < state.next.length; j += 2) {
      let node = state.next[j], next = state.next[j + 1]
      nodes.push(node.name)
      if (dead && !(node.isText || node.hasRequiredAttrs())) dead = false
      if (work.indexOf(next) == -1) work.push(next)
    }
    if (dead) stream.err("Only non-generatable nodes (" + nodes.join(", ") + ") in a required position")
  }
}
<commit_msg>Generate simple DFAs for content expressions<commit_after>import {Fragment} from "./fragment"

// ::- Instances of this class represent a match state of a node
// type's [content expression](#model.NodeSpec.content), and can be
// used to find out whether further content matches here, and whether
// a given position is a valid end of the node.
export class ContentMatch {
  constructor(validEnd) {
    // :: bool
    // True when this match state represents a valid end of the node.
    this.validEnd = validEnd
    this.next = []
    this.wrapCache = []
  }

  static parse(string, nodeTypes) {
    let stream = new TokenStream(string, nodeTypes)
    if (stream.next == null) return ContentMatch.empty
    let expr = parseExpr(stream)
    if (stream.next) stream.err("Unexpected trailing text")
    let match = dfa(nfa(expr))
    checkForDeadEnds(match, stream)
    return match
  }

  // :: (NodeType) → ?ContentMatch
  // Match a node type, returning a match after that node if
  // successful.
  matchType(type) {
    for (let i = 0; i < this.next.length; i += 2)
      if (this.next[i] == type) return this.next[i + 1]
    return null
  }

  // :: (Fragment, ?number, ?number) → ?ContentMatch
  // Try to match a fragment. Returns the resulting match when
  // successful.
  matchFragment(frag, start = 0, end = frag.childCount) {
    let cur = this
    for (let i = start; cur && i < end; i++)
      cur = cur.matchType(frag.child(i).type)
    return cur
  }

  get inlineContent() {
    let first = this.next[0]
    return first ? first.isInline : false
  }

  get defaultType() {
    return this.next[0]
  }

  compatible(other) {
    for (let i = 0; i < this.next.length; i += 2)
      for (let j = 0; j < other.next.length; j += 2)
        if (this.next[i] == other.next[j]) return true
    return false
  }

  // :: (Fragment, bool, ?number) → ?Fragment
  // Try to match the given fragment, and if that fails, see if it can
  // be made to match by inserting nodes in front of it. When
  // successful, return a fragment of inserted nodes (which may be
  // empty if nothing had to be inserted). When `toEnd` is true, only
  // return a fragment if the resulting match goes to the end of the
  // content expression.
  fillBefore(after, toEnd = false, startIndex = 0) {
    let seen = [this]
    function search(match, types) {
      let finished = match.matchFragment(after, startIndex)
      if (finished && (!toEnd || finished.validEnd))
        return Fragment.from(types.map(tp => tp.createAndFill()))

      for (let i = 0; i < match.next.length; i += 2) {
        let type = match.next[i], next = match.next[i + 1]
        if (!(type.isText || type.hasRequiredAttrs()) && seen.indexOf(next) == -1) {
          seen.push(next)
          let found = search(next, types.concat(type))
          if (found) return found
        }
      }
    }

    return search(this, [])
  }

  // :: (NodeType) → ?[NodeType]
  // Find a set of wrapping node types that would allow a node of the
  // given type to appear at this position. The result may be empty
  // (when it fits directly) and will be null when no such wrapping
  // exists.
  findWrapping(target) {
    for (let i = 0; i < this.wrapCache.length; i += 2)
      if (this.wrapCache[i] == target) return this.wrapCache[i + 1]
    let computed = this.computeWrapping(target)
    this.wrapCache.push(target, computed)
    return computed
  }

  computeWrapping(target) {
    let seen = Object.create(null), active = [{match: this, type: null, via: null}]
    while (active.length) {
      let current = active.shift(), match = current.match
      if (match.matchType(target)) {
        let result = []
        for (let obj = current; obj.type; obj = obj.via)
          result.push(obj.type)
        return result.reverse()
      }
      for (let i = 0; i < match.next.length; i += 2) {
        let type = match.next[i]
        if (!type.isLeaf && !(type.name in seen) && (!current.type || match.next[i + 1].validEnd)) {
          active.push({match: type.contentMatch, type, via: current})
          seen[type.name] = true
        }
      }
    }
  }

  // :: number
  // The number of outgoing edges this node has in the finite
  // automaton that describes the content expression.
  get edgeCount() {
    return this.next.length >> 1
  }

  // :: (number) → {type: NodeType, next: ContentMatch}
  // Get the _n_th outgoing edge from this node in the finite
  // automaton that describes the content expression.
  edge(n) {
    let i = n << 1
    if (i > this.next.length) throw new RangeError(`There's no ${n}th edge in this content match`)
    return {type: this.next[i], next: this.next[i + 1]}
  }

  toString() {
    let seen = []
    function scan(m) {
      seen.push(m)
      for (let i = 1; i < m.next.length; i += 2)
        if (seen.indexOf(m.next[i]) == -1) scan(m.next[i])
    }
    scan(this)
    return seen.map((m, i) => {
      let out = i + (m.validEnd ? "*" : " ") + " "
      for (let i = 0; i < m.next.length; i += 2)
        out += (i ? ", " : "") + m.next[i].name + "->" + seen.indexOf(m.next[i + 1])
      return out
    }).join("\n")
  }
}

ContentMatch.empty = new ContentMatch(true)

class TokenStream {
  constructor(string, nodeTypes) {
    this.string = string
    this.nodeTypes = nodeTypes
    this.inline = null
    this.pos = 0
    this.tokens = string.split(/\s*(?=\b|\W|$)/)
    if (this.tokens[this.tokens.length - 1] == "") this.tokens.pop()
    if (this.tokens[0] == "") this.tokens.unshift()
  }

  get next() { return this.tokens[this.pos] }

  eat(tok) { return this.next == tok && (this.pos++ || true) }

  err(str) { throw new SyntaxError(str + " (in content expression '" + this.string + "')") }
}

function parseExpr(stream) {
  let exprs = []
  do { exprs.push(parseExprSeq(stream)) }
  while (stream.eat("|"))
  return exprs.length == 1 ? exprs[0] : {type: "choice", exprs}
}

function parseExprSeq(stream) {
  let exprs = []
  do { exprs.push(parseExprSubscript(stream)) }
  while (stream.next && stream.next != ")" && stream.next != "|")
  return exprs.length == 1 ? exprs[0] : {type: "seq", exprs}
}

function parseExprSubscript(stream) {
  let expr = parseExprAtom(stream)
  for (;;) {
    if (stream.eat("+"))
      expr = {type: "plus", expr}
    else if (stream.eat("*"))
      expr = {type: "star", expr}
    else if (stream.eat("?"))
      expr = {type: "opt", expr}
    else if (stream.eat("{"))
      expr = parseExprRange(stream, expr)
    else break
  }
  return expr
}

function parseNum(stream) {
  if (/\D/.test(stream.next)) stream.err("Expected number, got '" + stream.next + "'")
  let result = Number(stream.next)
  stream.pos++
  return result
}

function parseExprRange(stream, expr) {
  let min = parseNum(stream), max = min
  if (stream.eat(",")) {
    if (stream.next != "}") max = parseNum(stream)
    else max = -1
  }
  if (!stream.eat("}")) stream.err("Unclosed braced range")
  return {type: "range", min, max, expr}
}

function resolveName(stream, name) {
  let types = stream.nodeTypes, type = types[name]
  if (type) return [type]
  let result = []
  for (let typeName in types) {
    let type = types[typeName]
    if (type.groups.indexOf(name) > -1) result.push(type)
  }
  if (result.length == 0) stream.err("No node type or group '" + name + "' found")
  return result
}

function parseExprAtom(stream) {
  if (stream.eat("(")) {
    let expr = parseExpr(stream)
    if (!stream.eat(")")) stream.err("Missing closing paren")
    return expr
  } else if (!/\W/.test(stream.next)) {
    let exprs = resolveName(stream, stream.next).map(type => {
      if (stream.inline == null) stream.inline = type.isInline
      else if (stream.inline != type.isInline) stream.err("Mixing inline and block content")
      return {type: "name", value: type}
    })
    stream.pos++
    return exprs.length == 1 ? exprs[0] : {type: "choice", exprs}
  } else {
    stream.err("Unexpected token '" + stream.next + "'")
  }
}

// The code below helps compile a regular-expression-like language
// into a deterministic finite automaton. For a good introduction to
// these concepts, see https://swtch.com/~rsc/regexp/regexp1.html

// : (Object) → [[{term: ?any, to: number}]]
// Construct an NFA from an expression as returned by the parser. The
// NFA is represented as an array of states, which are themselves
// arrays of edges, which are `{term, to}` objects. The first state is
// the entry state and the last node is the success state.
//
// Note that unlike typical NFAs, the edge ordering in this one is
// significant, in that it is used to contruct filler content when
// necessary.
function nfa(expr) {
  let nfa = [[]]
  connect(compile(expr, 0), node())
  return nfa

  function node() { return nfa.push([]) - 1 }
  function edge(from, to, term) {
    let edge = {term, to}
    nfa[from].push(edge)
    return edge
  }
  function connect(edges, to) { edges.forEach(edge => edge.to = to) }

  function compile(expr, from) {
    if (expr.type == "choice") {
      return expr.exprs.reduce((out, expr) => out.concat(compile(expr, from)), [])
    } else if (expr.type == "seq") {
      for (let i = 0;; i++) {
        let next = compile(expr.exprs[i], from)
        if (i == expr.exprs.length - 1) return next
        connect(next, from = node())
      }
    } else if (expr.type == "star") {
      let loop = node()
      edge(from, loop)
      connect(compile(expr.expr, loop), loop)
      return [edge(loop)]
    } else if (expr.type == "plus") {
      let loop = node()
      connect(compile(expr.expr, from), loop)
      connect(compile(expr.expr, loop), loop)
      return [edge(loop)]
    } else if (expr.type == "opt") {
      return [edge(from)].concat(compile(expr.expr, from))
    } else if (expr.type == "range") {
      let cur = from
      for (let i = 0; i < expr.min; i++) {
        let next = node()
        connect(compile(expr.expr, cur), next)
        cur = next
      }
      if (expr.max == -1) {
        connect(compile(expr.expr, cur), cur)
      } else {
        for (let i = expr.min; i < expr.max; i++) {
          let next = node()
          edge(cur, next)
          connect(compile(expr.expr, cur), next)
          cur = next
        }
      }
      return [edge(cur)]
    } else if (expr.type == "name") {
      return [edge(from, null, expr.value)]
    }
  }
}

function cmp(a, b) { return a - b }

// Get the set of nodes reachable by null edges from `node`. Omit
// nodes with only a single null-out-edge, since they may lead to
// needless duplicated nodes.
function nullFrom(nfa, node) {
  let result = []
  scan(node)
  return result.sort(cmp)

  function scan(node) {
    let edges = nfa[node]
    if (edges.length == 1 && !edges[0].term) return scan(edges[0].to)
    result.push(node)
    for (let i = 0; i < edges.length; i++) {
      let {term, to} = edges[i]
      if (!term && result.indexOf(to) == -1) scan(to)
    }
  }
}

// : ([[{term: ?any, to: number}]]) → ContentMatch
// Compiles an NFA as produced by `nfa` into a DFA, modeled as a set
// of state objects (`ContentMatch` instances) with transitions
// between them.
function dfa(nfa) {
  let labeled = Object.create(null)
  return explore(nullFrom(nfa, 0))

  function explore(states) {
    let out = []
    states.forEach(node => {
      nfa[node].forEach(({term, to}) => {
        if (!term) return
        let known = out.indexOf(term), set = known > -1 && out[known + 1]
        nullFrom(nfa, to).forEach(node => {
          if (!set) out.push(term, set = [])
          if (set.indexOf(node) == -1) set.push(node)
        })
      })
    })
    let state = labeled[states.join(",")] = new ContentMatch(states.indexOf(nfa.length - 1) > -1)
    for (let i = 0; i < out.length; i += 2) {
      let states = out[i + 1].sort(cmp)
      state.next.push(out[i], labeled[states.join(",")] || explore(states))
    }
    return state
  }
}

function checkForDeadEnds(match, stream) {
  for (let i = 0, work = [match]; i < work.length; i++) {
    let state = work[i], dead = !state.validEnd, nodes = []
    for (let j = 0; j < state.next.length; j += 2) {
      let node = state.next[j], next = state.next[j + 1]
      nodes.push(node.name)
      if (dead && !(node.isText || node.hasRequiredAttrs())) dead = false
      if (work.indexOf(next) == -1) work.push(next)
    }
    if (dead) stream.err("Only non-generatable nodes (" + nodes.join(", ") + ") in a required position")
  }
}
<|endoftext|>


<commit_before>/**
 * Created by joel on 5/18/16.
 */

"use strict";

let callback = null;
let SELECTING = false;
let svg = null;
let currentId = 0;
let clicked = false;

// assemble container element
const container = document.createElement('div');
container.id = 'visual-history-container';

// attach container element to document body wherever convenient
if (document.body.firstChild)
    document.body.insertBefore(container, document.body.firstChild);
else
    document.body.appendChild(container);

// close on loose click
container.onclick = e => exit(false);

// track right-clicked elements for the contextMenu
document.documentElement.addEventListener('mousedown', e =>
    e.button === 2 && e.target.textContent ? clicked = e.target.textContent : null, true);

function makeTree(data) {
    const imageSize = data.imageSize;
    const nodeWidth = data.nodeWidth;

    // freeze the document body and class the container
    document.body.classList.add('visual-history-freeze');
    container.className = 'visual-history-container-visible';

    container.style.setProperty("top", String(document.body.scrollTop) + 'px', "important");
    container.style.setProperty("left", String(document.body.scrollLeft) + 'px', "important");

    // start appending all the things

    // svg element contains a single wrapper g cell
    svg = d3_selection.select('#visual-history-container')
        .append('svg')
        .attr('width', data.width)
        .attr('height', data.height)
        .attr('id', 'visual-history-svg')
        .append('g');

    // append and class links
    const links = svg.selectAll('path').data(data.links, link => link.id);
    links.enter().append('line')
        .attr("class", "visual-history-link")
        .attr("x1", link => link.source.x)
        .attr("y1", link => link.source.y)
        .attr("x2", link => link.target.x)
        .attr("y2", link => link.target.y);

    // append and class nodes, and save the selector createNode for later
    const nodes = svg.selectAll('g').data(data.nodes, node => node.id);
    const createNode = nodes.enter().append('g')
        .attr('transform', node => `translate(${node.position.x},${node.position.y})`)
        .classed('visual-history-current', node => node.id === data.current.id)
        .classed('visual-history-preview', node => node.data.preview)
        .classed('visual-history-node', true)
        .attr('id', node => `visual-history-id-${node.id}`)
        .on('click', click);

    // append favIcon images to the node g cells
    createNode.append('image')
        .attr('x', `-${imageSize / 2.0}px`)
        .attr('y', `-${imageSize / 2.0}px`)
        .attr('width', `${imageSize}px`)
        .attr('height', `${imageSize}px`)
        .attr('xlink:href', node => node.data.favIconUrl)
        .attr('class', 'visual-history-image');

    // append an html wrapper div to the node g cells and save the selector to later
    const div = createNode.append('foreignObject')
        .attr('x', `-${nodeWidth / 2.0}px`)
        .attr('y', `${imageSize / 2.0}px`)
        .attr('width', `${nodeWidth}px`)
        .attr('height', `${imageSize}px`)
        .append('xhtml:div')
        .attr('class', 'visual-history-text');

    // append the title html element to the wrapper
    div.append('p')
        .attr('class', 'visual-history-title')
        .html(node => node.data.title || makeTitle(node.data.url));

    // append the url html element to the wrapper
    div.append('p')
        .attr('class', 'visual-history-url')
        .html(node => node.data.url);

    // class the current node and center position
    updateCurrent(data.current);

    // the day will come when I understand CSS animations
    container.style.opacity = 1;
}

function removeTree() {
    SELECTING = false;
    container.removeChild(document.getElementById('visual-history-svg'));
    container.className = '';
    svg = null;
    document.body.classList.remove('visual-history-freeze');

    container.style.opacity = 0;
    // but today is not that day.
}

function updateCurrent(current) {
    // class the current node
    d3_selection.select(`#visual-history-id-${current.id}`)
        .classed('visual-history-current', true)
        .classed('visual-history-preview', false);
    if (current.id !== currentId) {
        d3_selection.select(`#visual-history-id-${currentId}`).classed('visual-history-current', false);
        currentId = current.id;
    }

    // center current position
    container.scrollLeft = current.x - document.body.clientWidth / 2.0;
    container.scrollTop = current.y - document.body.clientHeight / 2.0;
}

function click(node) {
    // this is the only time we callback with a url
    // in all other cases, backend.js already knows
    // about the changes and will update accordingly
    exit(node.data.url);
}

function makeTitle(url) {
    // TODO: add fancy formatting for url titles
    return url;
}

document.documentElement.onkeyup = e => {
    const key = e.keyCode || e.which;
    // exit on Ctrl or Cmd release
    if (key === 17 || e.metaKey) exit(false);
};

document.documentElement.onkeydown = e => {
    const key = e.keyCode || e.which;
    // exit on Escape down
    if (key === 27 || e.code === 'Escape') exit(false);
};

function exit(url) {
    if (SELECTING) {
        removeTree();
        // make sure to seal the free closure upon exiting the building.
        callback({url: url});
    }
}

chrome.runtime.onMessage.addListener((data, sender, sendResponse) => {
    if (data.type === 'clicked') sendResponse({text: clicked});

    // callback on every other browserAction click.
    else if (data.type === 'close') exit(false);

    // moving the current pointer doesn't evaluate the closure
    else if (data.type === 'move' && SELECTING) updateCurrent(data.current);

    // do all the things
    else if (data.type === 'tree' && !SELECTING) {
        SELECTING = true;
        callback = sendResponse;
        makeTree(data);

        // only let one closure escape at a time
        return true;
    }
});<commit_msg>Bug fix for OS X compatibility<commit_after>/**
 * Created by joel on 5/18/16.
 */

"use strict";

let callback = null;
let SELECTING = false;
let svg = null;
let currentId = 0;
let clicked = false;

// assemble container element
const container = document.createElement('div');
container.id = 'visual-history-container';

// attach container element to document body wherever convenient
if (document.body.firstChild)
    document.body.insertBefore(container, document.body.firstChild);
else
    document.body.appendChild(container);

// close on loose click
container.onclick = e => exit(false);

// track right-clicked elements for the contextMenu
document.documentElement.addEventListener('mousedown', e =>
    e.button === 2 && e.target.textContent ? clicked = e.target.textContent : null, true);

function makeTree(data) {
    const imageSize = data.imageSize;
    const nodeWidth = data.nodeWidth;

    // freeze the document body and class the container
    document.body.classList.add('visual-history-freeze');
    container.className = 'visual-history-container-visible';

    container.style.setProperty("top", String(document.body.scrollTop) + 'px', "important");
    container.style.setProperty("left", String(document.body.scrollLeft) + 'px', "important");

    // start appending all the things

    // svg element contains a single wrapper g cell
    svg = d3_selection.select('#visual-history-container')
        .append('svg')
        .attr('width', data.width)
        .attr('height', data.height)
        .attr('id', 'visual-history-svg')
        .append('g');

    // append and class links
    const links = svg.selectAll('path').data(data.links, link => link.id);
    links.enter().append('line')
        .attr("class", "visual-history-link")
        .attr("x1", link => link.source.x)
        .attr("y1", link => link.source.y)
        .attr("x2", link => link.target.x)
        .attr("y2", link => link.target.y);

    // append and class nodes, and save the selector createNode for later
    const nodes = svg.selectAll('g').data(data.nodes, node => node.id);
    const createNode = nodes.enter().append('g')
        .attr('transform', node => `translate(${node.position.x},${node.position.y})`)
        .classed('visual-history-current', node => node.id === data.current.id)
        .classed('visual-history-preview', node => node.data.preview)
        .classed('visual-history-node', true)
        .attr('id', node => `visual-history-id-${node.id}`)
        .on('click', click);

    // append favIcon images to the node g cells
    createNode.append('image')
        .attr('x', `-${imageSize / 2.0}px`)
        .attr('y', `-${imageSize / 2.0}px`)
        .attr('width', `${imageSize}px`)
        .attr('height', `${imageSize}px`)
        .attr('xlink:href', node => node.data.favIconUrl)
        .attr('class', 'visual-history-image');

    // append an html wrapper div to the node g cells and save the selector to later
    const div = createNode.append('foreignObject')
        .attr('x', `-${nodeWidth / 2.0}px`)
        .attr('y', `${imageSize / 2.0}px`)
        .attr('width', `${nodeWidth}px`)
        .attr('height', `${imageSize}px`)
        .append('xhtml:div')
        .attr('class', 'visual-history-text');

    // append the title html element to the wrapper
    div.append('p')
        .attr('class', 'visual-history-title')
        .html(node => node.data.title || makeTitle(node.data.url));

    // append the url html element to the wrapper
    div.append('p')
        .attr('class', 'visual-history-url')
        .html(node => node.data.url);

    // class the current node and center position
    updateCurrent(data.current);

    // the day will come when I understand CSS animations
    container.style.opacity = 1;
}

function removeTree() {
    SELECTING = false;
    container.removeChild(document.getElementById('visual-history-svg'));
    container.className = '';
    svg = null;
    document.body.classList.remove('visual-history-freeze');

    container.style.opacity = 0;
    // but today is not that day.
}

function updateCurrent(current) {
    // class the current node
    d3_selection.select(`#visual-history-id-${current.id}`)
        .classed('visual-history-current', true)
        .classed('visual-history-preview', false);
    if (current.id !== currentId) {
        d3_selection.select(`#visual-history-id-${currentId}`).classed('visual-history-current', false);
        currentId = current.id;
    }

    // center current position
    container.scrollLeft = current.x - document.body.clientWidth / 2.0;
    container.scrollTop = current.y - document.body.clientHeight / 2.0;
}

function click(node) {
    // this is the only time we callback with a url
    // in all other cases, backend.js already knows
    // about the changes and will update accordingly
    exit(node.data.url);
}

function makeTitle(url) {
    // TODO: add fancy formatting for url titles
    return url;
}

document.documentElement.onkeyup = e => {
    const key = e.keyCode || e.which;
    // exit on Ctrl or Cmd release
    if (key === 17 || e.key === 'Mega' || e.metaKey) exit(false);
};

document.documentElement.onkeydown = e => {
    const key = e.keyCode || e.which;
    // exit on Escape down
    if (key === 27 || e.code === 'Escape') exit(false);
};

function exit(url) {
    if (SELECTING) {
        removeTree();
        // make sure to seal the free closure upon exiting the building.
        callback({url: url});
    }
}

chrome.runtime.onMessage.addListener((data, sender, sendResponse) => {
    if (data.type === 'clicked') sendResponse({text: clicked});

    // callback on every other browserAction click.
    else if (data.type === 'close') exit(false);

    // moving the current pointer doesn't evaluate the closure
    else if (data.type === 'move' && SELECTING) updateCurrent(data.current);

    // do all the things
    else if (data.type === 'tree' && !SELECTING) {
        SELECTING = true;
        callback = sendResponse;
        makeTree(data);

        // only let one closure escape at a time
        return true;
    }
});
<|endoftext|>


<commit_before>import Parse from 'parse';

export default class Item extends Parse.Object {

    constructor(data) {
        super('Item');

        if (data) {
            this.id = data.id;
            this.set('product', data.get('product'));
            this.price = this.product.price;
        }

        this.IVA_PERCENTAGE = 0.16;
    }

    get product() {
        return this.get('product');
    }

    set product(product) {
        this.set('product', product);
        this.price = this.product.price;
    }

    get sku() {
        return this.product.get('sku');
    }

    get description() {
        return this.product.get('description');
    }

    get quantity() {
        return this.get('quantity');
    }

    set quantity(value) {
        this.set('quantity', value);
    }

    get price() {
        return this.get('price');
    }

    set price(value) {
        this.set('price', value);
    }

    get floatPrice() {
        return this.price / 100;
    }

    set floatPrice(value) {
        this.price = value * 100;
    }

    get subtotals() {
        return this.price * this.quantity;
    }

    get iva() {
        return this.product.get('noIVA') ? 0 : (this.subtotals - this.discountValue) * this.IVA_PERCENTAGE;
    }

    get discount() {

        return this.get('discount');
    }

    set discount(value) {
        this.set('discount', value);
    }

    get discountValue() {
        return this.subtotals * (this.discount / 100);
    }

    get totals() {
        return this.subtotals - this.discountValue + this.iva;
    }

    get status() {
        return 'CAPTURADO';
    }

}

Parse.Object.registerSubclass('Item', Item);
<commit_msg>SAT-41: Added SKU field on Item model.<commit_after>import Parse from 'parse';

export default class Item extends Parse.Object {

    constructor(data) {
        super('Item');

        if (data) {
            this.id = data.id;
            this.set('product', data.get('product'));
            this.price = this.product.price;
        }

        this.IVA_PERCENTAGE = 0.16;
    }

    get product() {
        return this.get('product');
    }

    set product(product) {
        this.set('product', product);
        this.set('sku', product.get('sku'));
        this.price = this.product.price;
    }

    get sku() {
        return this.product.get('sku');
    }

    get description() {
        return this.product.get('description');
    }

    get quantity() {
        return this.get('quantity');
    }

    set quantity(value) {
        this.set('quantity', value);
    }

    get price() {
        return this.get('price');
    }

    set price(value) {
        this.set('price', value);
    }

    get floatPrice() {
        return this.price / 100;
    }

    set floatPrice(value) {
        this.price = value * 100;
    }

    get subtotals() {
        return this.price * this.quantity;
    }

    get iva() {
        return this.product.get('noIVA') ? 0 : (this.subtotals - this.discountValue) * this.IVA_PERCENTAGE;
    }

    get discount() {

        return this.get('discount');
    }

    set discount(value) {
        this.set('discount', value);
    }

    get discountValue() {
        return this.subtotals * (this.discount / 100);
    }

    get totals() {
        return this.subtotals - this.discountValue + this.iva;
    }

    get status() {
        return 'CAPTURADO';
    }

}

Parse.Object.registerSubclass('Item', Item);
<|endoftext|>


<commit_before>/*eslint-disable react/prop-types*/

import React from 'react';
import CounterViewContainer from './counter/CounterViewContainer';
import ColorViewContainer from './colors/ColorViewContainer';
import ExampleViewContainer from './exampleView/ExampleViewContainer';
import LoginViewContainer from './loginView/LoginViewContainer';
import FeedbackViewContainer from './feedbackView/FeedbackViewContainer';
import RegisterViewContainer from './RegisterView/RegisterViewContainer';
import MapViewContainer from './MapView/MapViewContainer';
import TeamViewContainer from './team/TeamViewContainer';
import WelcomeViewContainer from './WelcomeView/WelcomeViewContainer';
import CheckPointViewContainer from './checkpoints/CheckPointViewContainer';
import GoodbyeViewContainer from './Goodbye/GoodbyeViewContainer';
import TeamPointsViewContainer from './TeamPointsView/TeamPointsViewContainer';

/**
 * AppRouter is responsible for mapping a navigator scene to a view
 */
export default function AppRouter(props) {
  const key = props.scene.route.key;

  if (key === 'Counter') {
    return <CounterViewContainer />;
  }

  if (key.indexOf('Color') === 0) {
    const index = props.scenes.indexOf(props.scene);
    return (
      <ColorViewContainer
        index={index}
      />
    );
  }
  if (key === 'ExampleView') {
     return <ExampleViewContainer />;
   }

   if (key === 'LoginView') {
      return <LoginViewContainer />;
    }

    if (key === 'FeedbackView') {
       return <FeedbackViewContainer />;
     }

   if (key === 'RegisterView') {
      return <RegisterViewContainer />;
    }

    if (key === 'Welcome') {
      return <WelcomeViewContainer />;
    }

  if (key === 'MapView') {
    return <MapViewContainer />;
  }
  if (key === 'CheckPoints') {
  return <CheckPointViewContainer />;
  }

  if (key === 'TeamView') {
    return <TeamViewContainer />;
  }

  if (key === 'Goodbye') {
    return <GoodbyeViewContainer />;
  }

  if (key === 'TeamPointsView') {
    return <TeamPointsViewContainer />;
  }

  throw new Error('Unknown navigation key: ' + key);
}
<commit_msg>routeri<commit_after>/*eslint-disable react/prop-types*/

import React from 'react';
import CounterViewContainer from './counter/CounterViewContainer';
import ColorViewContainer from './colors/ColorViewContainer';
import ExampleViewContainer from './exampleView/ExampleViewContainer';
import LoginViewContainer from './login/LoginViewContainer';
import FeedbackViewContainer from './feedback/FeedbackViewContainer';
import RegisterViewContainer from './register/RegisterViewContainer';
import MapViewContainer from './MapView/MapViewContainer';
import TeamViewContainer from './team/TeamViewContainer';
import WelcomeViewContainer from './welcome/WelcomeViewContainer';
import CheckPointViewContainer from './checkpoints/CheckPointViewContainer';
import GoodbyeViewContainer from './goodbye/GoodbyeViewContainer';
import TeamPointsViewContainer from './teamPoints/TeamPointsViewContainer';

/**
 * AppRouter is responsible for mapping a navigator scene to a view
 */
export default function AppRouter(props) {
  const key = props.scene.route.key;

  if (key === 'Counter') {
    return <CounterViewContainer />;
  }

  if (key.indexOf('Color') === 0) {
    const index = props.scenes.indexOf(props.scene);
    return (
      <ColorViewContainer
        index={index}
      />
    );
  }
  if (key === 'ExampleView') {
     return <ExampleViewContainer />;
   }

   if (key === 'LoginView') {
      return <LoginViewContainer />;
    }

    if (key === 'FeedbackView') {
       return <FeedbackViewContainer />;
     }

   if (key === 'RegisterView') {
      return <RegisterViewContainer />;
    }

    if (key === 'Welcome') {
      return <WelcomeViewContainer />;
    }

  if (key === 'MapView') {
    return <MapViewContainer />;
  }
  if (key === 'CheckPoints') {
  return <CheckPointViewContainer />;
  }

  if (key === 'TeamView') {
    return <TeamViewContainer />;
  }

  if (key === 'Goodbye') {
    return <GoodbyeViewContainer />;
  }

  if (key === 'TeamPointsView') {
    return <TeamPointsViewContainer />;
  }

  throw new Error('Unknown navigation key: ' + key);
}
<|endoftext|>


<commit_before>// puppet.js 0.1.0
// (c) 2013 Joachim Wester
// MIT license

(function (global) {

  var lastClickHandler
    , lastPopstateHandler
    , lastBlurHandler;

  /**
   * Defines a connection to a remote PATCH server, returns callback to a object that is persistent between browser and server
   * @param remoteUrl If undefined, current window.location.href will be used as the PATCH server URL
   * @param callback Called after initial state object is received from the server
   * @param obj Optional object where the parsed JSON data will be inserted
   */
  function Puppet(remoteUrl, callback, obj) {
    this.debug = true;
    this.remoteUrl = remoteUrl;
    this.callback = callback;
    this.obj = obj || {};
    this.observer = null;
    this.referer = null;
    this.queue = [];
    this.handleResponseCookie();
    this.xhr(this.remoteUrl, 'application/json', null, this.bootstrap.bind(this));
  }

  /**
   * PuppetJsClickTrigger$ contains Unicode symbols for "NULL" text rendered stylized using Unicode
   * character "SYMBOL FOR NULL" (2400)
   *
   * With PuppetJs, any property having `null` value will be rendered as stylized "NULL" text
   * to emphasize that it probably should be set as empty string instead.
   *
   * The benefit of having this string is that any local change to `null` value (also
   * from `null` to `null`) can be detected and sent as `null` to the server.
   */
  var PuppetJsClickTrigger$ = "\u2400";

  function markObjPropertyByPath(obj, path) {
    var keys = path.split('/');
    var len = keys.length;
    if (keys.length > 2) {
      for (var i = 1; i < len - 1; i++) {
        obj = obj[keys[i]];
      }
    }
    recursiveMarkObjProperties(obj, keys[len - 1]);
  }

  function placeMarkers(parent, key) {
    var subject = parent[key];
    if (subject === null) {
      parent[key] = PuppetJsClickTrigger$;
    }
    else if (typeof subject === 'object' && !subject.hasOwnProperty('$parent')) {
      Object.defineProperty(subject, '$parent', {
        enumerable: false,
        get: function () {
          return parent;
        }
      });
    }
  }

  function recursiveMarkObjProperties(parent, key) {
    if (key && parent) {
      placeMarkers(parent, key);
      parent = parent[key];
    }

    if (parent) {
      for (var i in parent) {
        if (parent.hasOwnProperty(i) && typeof parent[i] === 'object') {
          recursiveMarkObjProperties(parent, i);
        }
      }
    }
  }

  function recursiveExtend(par, obj) {
    for(var i in obj) {
      if(obj.hasOwnProperty(i)) {
        if(typeof obj[i] === 'object' && par.hasOwnProperty(i)) {
          recursiveExtend(par[i], obj[i]);
        }
        else {
          par[i] = obj[i];
        }
      }
    }
  }

  Puppet.prototype.bootstrap = function (event) {
    var tmp = JSON.parse(event.target.responseText);
    recursiveExtend(this.obj, tmp);

    recursiveMarkObjProperties(this.obj);
    this.observe();
    if (this.callback) {
      this.callback(this.obj);
    }
    if (lastClickHandler) {
      document.body.removeEventListener('click', lastClickHandler);
      window.removeEventListener('popstate', lastPopstateHandler);
      document.body.removeEventListener('blur', lastBlurHandler, true);
    }
    document.body.addEventListener('click', lastClickHandler = this.clickHandler.bind(this));
    window.addEventListener('popstate', lastPopstateHandler = this.historyHandler.bind(this)); //better here than in constructor, because Chrome triggers popstate on page load
    document.body.addEventListener('blur', lastBlurHandler = this.sendLocalChange.bind(this), true);
  };

  Puppet.prototype.handleResponseHeader = function (xhr) {
    var location = xhr.getResponseHeader('X-Location') || xhr.getResponseHeader('Location');
    if (location) {
      this.referer = location;
    }
  };

  /**
   * JavaScript cannot read HTTP "Location" header for the main HTML document, but it can read cookies
   * So if you want to establish session in the main HTML document, send "Location" value as a cookie
   * The cookie will be erased (replaced with empty value) after reading
   */
  Puppet.prototype.handleResponseCookie = function () {
    var location = cookie.read('Location');
    if (location) { //if cookie exists and is not empty
      this.referer = location;
      cookie.erase('Location');
    }
  };

  Puppet.prototype.observe = function () {
    this.observer = jsonpatch.observe(this.obj, this.queueLocalChange.bind(this));
  };

  Puppet.prototype.unobserve = function () {
    if (this.observer) { //there is a bug in JSON-Patch when trying to unobserve something that is already unobserved
      jsonpatch.unobserve(this.obj, this.observer);
      this.observer = null;
    }
  };

  Puppet.prototype.queueLocalChange = function (patches) {
    Array.prototype.push.apply(this.queue, patches);
    if ((document.activeElement.nodeName !== 'INPUT' && document.activeElement.nodeName !== 'TEXTAREA') || document.activeElement.getAttribute('update-on') === 'input') {
      if (this.queue.length) {
        this.flattenPatches(this.queue);
        this.handleLocalChange(this.queue);
        this.queue.length = 0;
      }
    }
  };

  Puppet.prototype.sendLocalChange = function () {
    jsonpatch.generate(this.observer);
    if (this.queue.length) {
      this.flattenPatches(this.queue);
      this.handleLocalChange(this.queue);
      this.queue.length = 0;
    }
  };

  //merges redundant patches
  Puppet.prototype.flattenPatches = function (patches) {
    var seen = {};
    for (var i = patches.length - 1; i >= 0; i--) {
      if (patches[i].op === 'replace') {
        if (seen[patches[i].path]) {
          patches.splice(i, 1);
        }
        else {
          seen[patches[i].path] = true;
        }
      }
    }
  };

  Puppet.prototype.handleLocalChange = function (patches) {
    var txt = JSON.stringify(patches);
    if (txt.indexOf('__Jasmine_been_here_before__') > -1) {
      throw new Error("PuppetJs did not handle Jasmine test case correctly");
    }
    //"referer" should be used as the url when sending JSON Patches (see https://github.com/PuppetJs/PuppetJs/wiki/Server-communication)
    this.xhr(this.referer || this.remoteUrl, 'application/json-patch+json', txt, this.handleRemoteChange.bind(this));
    var that = this;
    this.unobserve();
    patches.forEach(function (patch) {
      if ((patch.op === "add" || patch.op === "replace" || patch.op === "test") && patch.value === null) {
        patch.value = PuppetJsClickTrigger$;
        jsonpatch.apply(that.obj, [patch]);
      }
      markObjPropertyByPath(that.obj, patch.path);
    });
    this.observe();
  };

  Puppet.prototype.handleRemoteChange = function (event) {
    if (!this.observer) {
      return; //ignore remote change if we are not watching anymore
    }
    var patches = JSON.parse(event.target.responseText || '[]'); //fault tolerance - empty response string should be treated as empty patch array
    if (patches.length === void 0) {
      throw new Error("Patches should be an array");
    }
    this.unobserve();
    jsonpatch.apply(this.obj, patches);
    var that = this;
    patches.forEach(function (patch) {
      if (patch.op === "add" || patch.op === "replace" || patch.op === "test") {
        markObjPropertyByPath(that.obj, patch.path);
      }
    });
    this.observe();
    if (this.onRemoteChange) {
      this.onRemoteChange(patches);
    }
  };

  Puppet.prototype.isApplicationLink = function (href) {
    return (href.protocol == window.location.protocol && href.host == window.location.host);
  };

  Puppet.prototype.changeState = function (href) {
    this.xhr(href, 'application/json-patch+json', null, this.handleRemoteChange.bind(this));
  };

  Puppet.prototype.clickHandler = function (event) {
    if(event.detail && event.detail.target) {
      //detail is Polymer
      event = event.detail;
    }
    var target = event.target;
    if (window.PuppetExternalLink) {
      target = window.PuppetExternalLink;
      window.PuppetExternalLink = null;
    }
    if (target.href && this.isApplicationLink(target)) {
      event.preventDefault();
      this.morphUrl(target.href);
    }
    else if (target.type === 'submit') {
      event.preventDefault();
    }
    else {
      this.sendLocalChange(); //needed for checkbox
    }
  };

  Puppet.prototype.historyHandler = function (/*event*/) {
    this.changeState(location.href);
  };

  Puppet.prototype.showError = function (heading, description) {
    if (this.debug) {
      var DIV = document.getElementById('puppetjs-error');
      if(!DIV) {
        DIV = document.createElement('DIV');
        DIV.id = 'puppetjs-error';
        DIV.style.border = '1px solid #dFb5b4';
        DIV.style.background = '#fcf2f2';
        DIV.style.padding = '10px 16px';
        if (document.body.firstChild) {
          document.body.insertBefore(DIV, document.body.firstChild);
        }
        else {
          document.body.appendChild(DIV);
        }
      }

      var H1 = document.createElement('H1');
      H1.innerHTML = heading;

      var PRE = document.createElement('PRE');
      PRE.innerHTML = description;
      PRE.style.whiteSpace = 'pre-wrap';

      DIV.appendChild(H1);
      DIV.appendChild(PRE);
    }
    throw new Error(description);
  };

  Puppet.prototype.xhr = function (url, accept, data, callback) {
    //this.handleResponseCookie();
    cookie.erase('Location'); //more invasive cookie erasing because sometimes the cookie was still visible in the requests

    var req = new XMLHttpRequest();
    var that = this;
    req.addEventListener('load', function (event) {
      that.handleResponseCookie();
      that.handleResponseHeader(event.target);
      if (event.target.status >= 400 && event.target.status <= 599) {
        that.showError('PuppetJs JSON response error', 'Server responded with error ' + event.target.status + ' ' + event.target.statusText + '\n\n' + event.target.responseText);
      }
      else {
        callback.call(that, event);
      }
    }, false);
    url = url || window.location.href;
    if (data) {
      req.open("PATCH", url, true);
      req.setRequestHeader('Content-Type', 'application/json-patch+json');
    }
    else {
      req.open("GET", url, true);
    }
    if (accept) {
      req.setRequestHeader('Accept', accept);
    }
    if (this.referer) {
      req.setRequestHeader('X-Referer', this.referer);
    }
    req.send(data);
  };

  /**
   * Push a new URL to the browser address bar and send a patch request (empty or including queued local patches)
   * so that the URL handlers can be executed on the server
   * @param href
   */
  Puppet.prototype.morphUrl = function (url) {
    history.pushState(null, null, url);
    this.changeState(url);
  };

  Puppet.prototype.catchExternaLink = function (element) {
    element.addEventListener("click", function (event) {
      window.PuppetExternalLink = event.target;
    });
  };

  /**
   * Cookie helper
   * reference: http://www.quirksmode.org/js/cookies.html
   * reference: https://github.com/js-coder/cookie.js/blob/gh-pages/cookie.js
   */
  var cookie = {
    create: function createCookie(name, value, days) {
      var expires = "";
      if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = "; expires=" + date.toGMTString();
      }
      document.cookie = name + "=" + value + expires + '; path=/';
    },

    readAll: function readCookies() {
      if (document.cookie === '') return {};
      var cookies = document.cookie.split('; ')
        , result = {};
      for (var i = 0, l = cookies.length; i < l; i++) {
        var item = cookies[i].split('=');
        result[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
      }
      return result;
    },

    read: function readCookie(name) {
      return cookie.readAll()[name];
    },

    erase: function eraseCookie(name) {
      cookie.create(name, "", -1);
    }
  };

  global.Puppet = Puppet;
})(window);<commit_msg>formatting<commit_after>// puppet.js 0.1.0
// (c) 2013 Joachim Wester
// MIT license

(function (global) {

  var lastClickHandler
    , lastPopstateHandler
    , lastBlurHandler;

  /**
   * Defines a connection to a remote PATCH server, returns callback to a object that is persistent between browser and server
   * @param remoteUrl If undefined, current window.location.href will be used as the PATCH server URL
   * @param callback Called after initial state object is received from the server
   * @param obj Optional object where the parsed JSON data will be inserted
   */
  function Puppet(remoteUrl, callback, obj) {
    this.debug = true;
    this.remoteUrl = remoteUrl;
    this.callback = callback;
    this.obj = obj || {};
    this.observer = null;
    this.referer = null;
    this.queue = [];
    this.handleResponseCookie();
    this.xhr(this.remoteUrl, 'application/json', null, this.bootstrap.bind(this));
  }

  /**
   * PuppetJsClickTrigger$ contains Unicode symbols for "NULL" text rendered stylized using Unicode
   * character "SYMBOL FOR NULL" (2400)
   *
   * With PuppetJs, any property having `null` value will be rendered as stylized "NULL" text
   * to emphasize that it probably should be set as empty string instead.
   *
   * The benefit of having this string is that any local change to `null` value (also
   * from `null` to `null`) can be detected and sent as `null` to the server.
   */
  var PuppetJsClickTrigger$ = "\u2400";

  function markObjPropertyByPath(obj, path) {
    var keys = path.split('/');
    var len = keys.length;
    if (keys.length > 2) {
      for (var i = 1; i < len - 1; i++) {
        obj = obj[keys[i]];
      }
    }
    recursiveMarkObjProperties(obj, keys[len - 1]);
  }

  function placeMarkers(parent, key) {
    var subject = parent[key];
    if (subject === null) {
      parent[key] = PuppetJsClickTrigger$;
    }
    else if (typeof subject === 'object' && !subject.hasOwnProperty('$parent')) {
      Object.defineProperty(subject, '$parent', {
        enumerable: false,
        get: function () {
          return parent;
        }
      });
    }
  }

  function recursiveMarkObjProperties(parent, key) {
    if (key && parent) {
      placeMarkers(parent, key);
      parent = parent[key];
    }

    if (parent) {
      for (var i in parent) {
        if (parent.hasOwnProperty(i) && typeof parent[i] === 'object') {
          recursiveMarkObjProperties(parent, i);
        }
      }
    }
  }

  function recursiveExtend(par, obj) {
    for (var i in obj) {
      if (obj.hasOwnProperty(i)) {
        if (typeof obj[i] === 'object' && par.hasOwnProperty(i)) {
          recursiveExtend(par[i], obj[i]);
        }
        else {
          par[i] = obj[i];
        }
      }
    }
  }

  Puppet.prototype.bootstrap = function (event) {
    var tmp = JSON.parse(event.target.responseText);
    recursiveExtend(this.obj, tmp);

    recursiveMarkObjProperties(this.obj);
    this.observe();
    if (this.callback) {
      this.callback(this.obj);
    }
    if (lastClickHandler) {
      document.body.removeEventListener('click', lastClickHandler);
      window.removeEventListener('popstate', lastPopstateHandler);
      document.body.removeEventListener('blur', lastBlurHandler, true);
    }
    document.body.addEventListener('click', lastClickHandler = this.clickHandler.bind(this));
    window.addEventListener('popstate', lastPopstateHandler = this.historyHandler.bind(this)); //better here than in constructor, because Chrome triggers popstate on page load
    document.body.addEventListener('blur', lastBlurHandler = this.sendLocalChange.bind(this), true);
  };

  Puppet.prototype.handleResponseHeader = function (xhr) {
    var location = xhr.getResponseHeader('X-Location') || xhr.getResponseHeader('Location');
    if (location) {
      this.referer = location;
    }
  };

  /**
   * JavaScript cannot read HTTP "Location" header for the main HTML document, but it can read cookies
   * So if you want to establish session in the main HTML document, send "Location" value as a cookie
   * The cookie will be erased (replaced with empty value) after reading
   */
  Puppet.prototype.handleResponseCookie = function () {
    var location = cookie.read('Location');
    if (location) { //if cookie exists and is not empty
      this.referer = location;
      cookie.erase('Location');
    }
  };

  Puppet.prototype.observe = function () {
    this.observer = jsonpatch.observe(this.obj, this.queueLocalChange.bind(this));
  };

  Puppet.prototype.unobserve = function () {
    if (this.observer) { //there is a bug in JSON-Patch when trying to unobserve something that is already unobserved
      jsonpatch.unobserve(this.obj, this.observer);
      this.observer = null;
    }
  };

  Puppet.prototype.queueLocalChange = function (patches) {
    Array.prototype.push.apply(this.queue, patches);
    if ((document.activeElement.nodeName !== 'INPUT' && document.activeElement.nodeName !== 'TEXTAREA') || document.activeElement.getAttribute('update-on') === 'input') {
      if (this.queue.length) {
        this.flattenPatches(this.queue);
        this.handleLocalChange(this.queue);
        this.queue.length = 0;
      }
    }
  };

  Puppet.prototype.sendLocalChange = function () {
    jsonpatch.generate(this.observer);
    if (this.queue.length) {
      this.flattenPatches(this.queue);
      this.handleLocalChange(this.queue);
      this.queue.length = 0;
    }
  };

  //merges redundant patches
  Puppet.prototype.flattenPatches = function (patches) {
    var seen = {};
    for (var i = patches.length - 1; i >= 0; i--) {
      if (patches[i].op === 'replace') {
        if (seen[patches[i].path]) {
          patches.splice(i, 1);
        }
        else {
          seen[patches[i].path] = true;
        }
      }
    }
  };

  Puppet.prototype.handleLocalChange = function (patches) {
    var txt = JSON.stringify(patches);
    if (txt.indexOf('__Jasmine_been_here_before__') > -1) {
      throw new Error("PuppetJs did not handle Jasmine test case correctly");
    }
    //"referer" should be used as the url when sending JSON Patches (see https://github.com/PuppetJs/PuppetJs/wiki/Server-communication)
    this.xhr(this.referer || this.remoteUrl, 'application/json-patch+json', txt, this.handleRemoteChange.bind(this));
    var that = this;
    this.unobserve();
    patches.forEach(function (patch) {
      if ((patch.op === "add" || patch.op === "replace" || patch.op === "test") && patch.value === null) {
        patch.value = PuppetJsClickTrigger$;
        jsonpatch.apply(that.obj, [patch]);
      }
      markObjPropertyByPath(that.obj, patch.path);
    });
    this.observe();
  };

  Puppet.prototype.handleRemoteChange = function (event) {
    if (!this.observer) {
      return; //ignore remote change if we are not watching anymore
    }
    var patches = JSON.parse(event.target.responseText || '[]'); //fault tolerance - empty response string should be treated as empty patch array
    if (patches.length === void 0) {
      throw new Error("Patches should be an array");
    }
    this.unobserve();
    jsonpatch.apply(this.obj, patches);
    var that = this;
    patches.forEach(function (patch) {
      if (patch.op === "add" || patch.op === "replace" || patch.op === "test") {
        markObjPropertyByPath(that.obj, patch.path);
      }
    });
    this.observe();
    if (this.onRemoteChange) {
      this.onRemoteChange(patches);
    }
  };

  Puppet.prototype.isApplicationLink = function (href) {
    return (href.protocol == window.location.protocol && href.host == window.location.host);
  };

  Puppet.prototype.changeState = function (href) {
    this.xhr(href, 'application/json-patch+json', null, this.handleRemoteChange.bind(this));
  };

  Puppet.prototype.clickHandler = function (event) {
    if (event.detail && event.detail.target) {
      //detail is Polymer
      event = event.detail;
    }
    var target = event.target;
    if (window.PuppetExternalLink) {
      target = window.PuppetExternalLink;
      window.PuppetExternalLink = null;
    }
    if (target.href && this.isApplicationLink(target)) {
      event.preventDefault();
      this.morphUrl(target.href);
    }
    else if (target.type === 'submit') {
      event.preventDefault();
    }
    else {
      this.sendLocalChange(); //needed for checkbox
    }
  };

  Puppet.prototype.historyHandler = function (/*event*/) {
    this.changeState(location.href);
  };

  Puppet.prototype.showError = function (heading, description) {
    if (this.debug) {
      var DIV = document.getElementById('puppetjs-error');
      if(!DIV) {
        DIV = document.createElement('DIV');
        DIV.id = 'puppetjs-error';
        DIV.style.border = '1px solid #dFb5b4';
        DIV.style.background = '#fcf2f2';
        DIV.style.padding = '10px 16px';
        if (document.body.firstChild) {
          document.body.insertBefore(DIV, document.body.firstChild);
        }
        else {
          document.body.appendChild(DIV);
        }
      }

      var H1 = document.createElement('H1');
      H1.innerHTML = heading;

      var PRE = document.createElement('PRE');
      PRE.innerHTML = description;
      PRE.style.whiteSpace = 'pre-wrap';

      DIV.appendChild(H1);
      DIV.appendChild(PRE);
    }
    throw new Error(description);
  };

  Puppet.prototype.xhr = function (url, accept, data, callback) {
    //this.handleResponseCookie();
    cookie.erase('Location'); //more invasive cookie erasing because sometimes the cookie was still visible in the requests

    var req = new XMLHttpRequest();
    var that = this;
    req.addEventListener('load', function (event) {
      that.handleResponseCookie();
      that.handleResponseHeader(event.target);
      if (event.target.status >= 400 && event.target.status <= 599) {
        that.showError('PuppetJs JSON response error', 'Server responded with error ' + event.target.status + ' ' + event.target.statusText + '\n\n' + event.target.responseText);
      }
      else {
        callback.call(that, event);
      }
    }, false);
    url = url || window.location.href;
    if (data) {
      req.open("PATCH", url, true);
      req.setRequestHeader('Content-Type', 'application/json-patch+json');
    }
    else {
      req.open("GET", url, true);
    }
    if (accept) {
      req.setRequestHeader('Accept', accept);
    }
    if (this.referer) {
      req.setRequestHeader('X-Referer', this.referer);
    }
    req.send(data);
  };

  /**
   * Push a new URL to the browser address bar and send a patch request (empty or including queued local patches)
   * so that the URL handlers can be executed on the server
   * @param href
   */
  Puppet.prototype.morphUrl = function (url) {
    history.pushState(null, null, url);
    this.changeState(url);
  };

  Puppet.prototype.catchExternaLink = function (element) {
    element.addEventListener("click", function (event) {
      window.PuppetExternalLink = event.target;
    });
  };

  /**
   * Cookie helper
   * reference: http://www.quirksmode.org/js/cookies.html
   * reference: https://github.com/js-coder/cookie.js/blob/gh-pages/cookie.js
   */
  var cookie = {
    create: function createCookie(name, value, days) {
      var expires = "";
      if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = "; expires=" + date.toGMTString();
      }
      document.cookie = name + "=" + value + expires + '; path=/';
    },

    readAll: function readCookies() {
      if (document.cookie === '') return {};
      var cookies = document.cookie.split('; ')
        , result = {};
      for (var i = 0, l = cookies.length; i < l; i++) {
        var item = cookies[i].split('=');
        result[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
      }
      return result;
    },

    read: function readCookie(name) {
      return cookie.readAll()[name];
    },

    erase: function eraseCookie(name) {
      cookie.create(name, "", -1);
    }
  };

  global.Puppet = Puppet;
})(window);<|endoftext|>


<commit_before>export default {
  userMenuAddressBook: "[data-test-id='menuAddressBookButton']",

  addressListPage: "[data-test-id='UserAddressBookPage']",
  addressListPageText: "[data-test-id='UserAddressBookPage'] > span",
  addressListAddButton: "[data-test-id='link: /user/address/0']",
  addressListPageAddress: (addressId) => `[data-test-id='link: /user/address/${addressId}']`,

  userAddressBookAddPage: "[data-test-id='UserAddressBookAddPage']",
  userAddressBookAddPageForm: "[data-test-id='UserAddressBookAddPage'] > form",
  userAddressBookAddPageBottomButton: "[data-test-id='UserAddressBookAddPage'] [data-test-id='Button']",

  modal: "[data-test-id='basicDialog']",
};
<commit_msg>PWA-1141: Fixed a linter error<commit_after>export default {
  userMenuAddressBook: "[data-test-id='menuAddressBookButton']",

  addressListPage: "[data-test-id='UserAddressBookPage']",
  addressListPageText: "[data-test-id='UserAddressBookPage'] > span",
  addressListAddButton: "[data-test-id='link: /user/address/0']",
  addressListPageAddress: addressId => `[data-test-id='link: /user/address/${addressId}']`,

  userAddressBookAddPage: "[data-test-id='UserAddressBookAddPage']",
  userAddressBookAddPageForm: "[data-test-id='UserAddressBookAddPage'] > form",
  userAddressBookAddPageBottomButton: "[data-test-id='UserAddressBookAddPage'] [data-test-id='Button']",

  modal: "[data-test-id='basicDialog']",
};
<|endoftext|>


<commit_before>var YouTyping = function (element, settings) {
	var youTyping = this;

	this.noteState = {
		WAITING: 0,
		HITTING: 1,
		CLEARED: 2,
		FAILED: 3
	};

	/******************* Internal functions *******************/

	var setupPlayerDeferred;
	var setupPlayer = function (callback) {
		setupPlayerDeferred = $.Deferred();
		logTrace('Setting Player Up...');

		var APITag = document.createElement('script');
		APITag.src = 'https://www.youtube.com/iframe_api';
		var firstScript = document.getElementsByTagName('script')[0];
		firstScript.parentNode.insertBefore(APITag, firstScript);

		return setupPlayerDeferred.promise();
	};

	// callback function used by YouTube IFrame Player API. must be global
	onYouTubeIframeAPIReady = function () {
		var settings = youTyping.settings;

		logTrace('Player API is Ready.');

		// try to hide advertisement if sandbox parameter is 'true' or not defined in URI query
		if (getParameterByName('sandbox') === 'true') {
			this.DOM.player.setAttribute('sandbox', 'allow-same-origin allow-scripts');
		}

		youTyping.player = new YT.Player('youtyping-player', {
			height: settings.height,
			width: settings.width,
			videoId: settings.videoId,
			playerVars: {
				rel: 0,
				// Wishing the best effort of hiding any information except for the video
				controls: 0,
				showinfo: 0,
				modestbranding: 1,
				wmode: 'opaque' // thanks http://stackoverflow.com/questions/6826386/
			},
			events: {
				'onReady': onPlayerReady,
				'onStateChange': onPlayerStateChange,
				'onError': onPlayerError
			}
		});
	};

	var onPlayerReady = function (event) {
		logTrace('Player is Ready.');
		setupPlayerDeferred.resolve();
	};

	var onPlayerStateChange = function (event) {
		switch (event.data) {
			case YT.PlayerState.ENDED:
				logTrace('Player Ended.');
				break;
			case YT.PlayerState.PLAYING:
				logTrace('Player Started.');
				break;
			case YT.PlayerState.PAUSED:
				logTrace('Player Paused.');
				break;
			case YT.PlayerState.BUFFERING:
				logTrace('Player Buffering.');
				break;
			case YT.PlayerState.CUED:
				logTrace('Player Cued.');
				break;
		}
	};

	var onPlayerError = function (event) {
		// from https://developers.google.com/youtube/iframe_api_reference
		switch (event.data) {
			case 2:
				logTrace('ERROR: The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.');
				break;
			case 5:
				logTrace('ERROR: The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.');
				break;
			case 100:
				logTrace('ERROR: The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.');
				break;
			case 101:
				logTrace('ERROR: The owner of the requested video does not allow it to be played in embedded players.');
				break;
			case 150:
				logTrace('ERROR: The owner of the requested video does not allow it to be played in embedded players.');
				break;
		}
		setupPlayerDeferred.reject();
	};

	var loadXMLDeferred;
	var loadScoreXML = function () {
		var settings = youTyping.settings;

		// Initialize deferred
		loadXMLDeferred = $.Deferred();

		$.ajax({
			url: settings.score,
			type: 'get',
			datatype: 'xml',
			timeout: 1000,
			success: function (data, textStatus, jqXHR) {
				youTyping.scoreXML = $(data).find('fumen').find('item');
				logTrace('Loaded XML File.');

				// parse XML and store into YouTyping.score
				youTyping.score = [];

				$(youTyping.scoreXML).each(function () {
					var tempItem = {
						time: parseFloat($(this).attr('time')) * 1000, // convert to millisecond
						type: $(this).attr('type')
					};

					if ($(this).attr('text')) {
						tempItem.text = $(this).attr('text');
					}

					if (tempItem.type === '+') {
						tempItem.state = youTyping.noteState.WAITING;
					}

					youTyping.score.push(tempItem);
				});

				loadXMLDeferred.resolve();
			},
			error: function (jqXHR, textStatus, errorThrown) {
				logTrace('ERROR: XML File Loading Failed: ' + errorThrown);
				loadXMLDeferred.reject();
			}
		});

		return loadXMLDeferred.promise();
	};

	/******************* properties *******************/

	this.startTime = Date.now();

	// score data
	this.scoreXML = null;
	this.score = null;

	// YouTube Iframe Player
	this.player = null;

	// default settings
	this.settings = {
		zeroEstimateSamples: 16, // integer
		videoId: 'fQ_m5VLhqNg',
		score: 'data.utfx',
		width: 1120, // pixel
		height: 630, // pixel
		hitPosition: 200, // pixel
		noteSize: 50, // pixel
		speed: 0.5, // pixel per second
		scoreYpos: 0.5, // ratio
		longLineHeight: 150, // pixel
		lineHeight: 120, // pixel
		screenPadding: 30, // pixel
		judges: {
			perfect: {
				from: -30,
				to: 30
			},
			great: {
				from: -50,
				to: 50
			},
			good: {
				from: -70,
				to: 70
			}
		}
	};

	// ZeroTime calculation
	this.zeroTime = 0;
	this.zeroTimePad = 0;
	this.currentTime = 0;
	this.estimateSamples = [];
	this.estimatedZero = 0; // exposed only for debugging
	this.zeroCallFPS = 0; // exposed only for debugging

	// YouTyping.now
	Object.defineProperty(this, 'now', {
		get: function () {
			return window.performance.now();
		}
	});

	/******************* Methods *******************/

	this.play = function () {
		youTyping.player.playVideo();

		// Set interval to calculate `ZeroTime`

		/***************

		# What's `ZeroTime` and `ZeroCall`?

		The current time taken from YouTube API by `getCurrentTime()`
		is resoluted very roughly (about 0.2s) with a great range of errors (about 0.05s).

		It's so fatal for music game like YouTyping. So we introduced idea that calibrates
		correct playing time by taking average of measuring. That's `ZeroTime`.

		YouTyping loops to get current playing time from API (to `gotCurrentTime`)
		with enough interval time (10ms) to detect when the `getCurrentTime()` time jumped up to another value.
		And each time `gotCurrentTime` jumped (nameed `ZeroCall`),
		YouTyping assumes the time to be correct and counts backward to estimate when this video started,
		so the time is nameed `ZeroTime`. Then the current playing time of video will be calculated by `ZeroTime` and
		current time taken from browser clock (very highly resoluted as <1ms).

		***************/
		setInterval(function () {
			var gotCurrentTime = youTyping.player.getCurrentTime();
			var now = youTyping.now;

			if (gotCurrentTime === 0) { // if playing time is zero `ZeroTime` is immediately `now`!
				youTyping.zeroTimePad = now;
				youTyping.zeroTime = now;
			} else if (youTyping.currentTime !== gotCurrentTime) { // if Current Time jumped
				youTyping.currentTime = gotCurrentTime;
				youTyping.estimatedZero = now - youTyping.currentTime * 1000;

				// Estimated zero time is stored in estimatesamples and
				// we assume that correct zero time is average of recent
				// `zeroEstimateSamples` items of samples
				// because it contains great ranges of error.
				// We also introduced `zeroTimePad` to supress a sudden change of zeroTime.
				// It contains correct zero time and sudden-change-supressed zero time
				// will be stored in `zeroTime`.
				youTyping.estimateSamples.push(youTyping.estimatedZero);
				if (youTyping.estimateSamples.length > youTyping.settings.zeroEstimateSamples) {
					youTyping.estimateSamples.shift();
				}
				// just go hack :)
				var estimatedSum = youTyping.estimateSamples.reduce(function (previous, current) {
					return previous + current;
				});

				// `zeroTimePad` is actual estimated ZeroTime and real displayed ZeroTime is modested into `zeroTime`.
				youTyping.zeroTimePad = estimatedSum / youTyping.estimateSamples.length;

				youTyping.zeroCallFPS++;
			}
			youTyping.zeroTime = (youTyping.zeroTime - youTyping.zeroTimePad) * 0.9 + youTyping.zeroTimePad;
		}, 10);
	};

	// hit key
	// TODO: make HitEvent interface
	this.hit = function (key, time) {
		if (!time) {
			time = youTyping.now;
		}
		var absoluteTime = time - youTyping.zeroTime;

		// search for nearest note that matches currently passed key rule
		var nearestNote = null;
		var nearestDistance = Infinity;
		youTyping.score.forEach(function (item, index) {
			if (item.type === '+') {
				if (item.state === youTyping.noteState.WAITING && Math.abs(absoluteTime - item.time) < Math.abs(nearestDistance)) {
					nearestNote = item;
					nearestDistance = item.time - absoluteTime;
				}
			}
		});

		var distance = nearestDistance;

		if (nearestNote !== null) {
			var hitJudge = null;

			for (var judgeName in youTyping.settings.judges) {
				if (youTyping.settings.judges.hasOwnProperty(judgeName)) {
					var judge = youTyping.settings.judges[judgeName];
					if (judge.from <= distance && distance <= judge.to) {
						hitJudge = judgeName;
						break;
					}
				}
			}

			if (hitJudge !== null) {
				nearestNote.state = youTyping.noteState.CLEARED;
				console.log(distance, hitJudge);
			}
		}
	};

	/******************* Initialization *******************/

	// override default settings
	for (var param in settings) {
		if (settings.hasOwnProperty(param)) {
			if (this.settings[param] === undefined) {
				this.settings[param] = settings[param];
			} else if (typeof this.settings[param] === 'number') {
				this.settings[param] = parseInt(settings[param], 10);
			} else {
				this.settings[param] = settings[param];
			}
		}
	}

	// setup DOM
	/*
	 * div(this.DOM.wrap)
	 * |-div#youtyping-player(this.DOM.player)
	 * \-canvas#youtyping-screen(this.DOM.screen)
	 */
	this.DOM = {
		wrap: element.css({
			width: this.settings.width + 'px',
			height: this.settings.height + 'px',
			margin: '0 auto',
			position: 'relative'
		}),

		player: $('<div/>', {
			id: 'youtyping-player'
		}).appendTo(element).css({
			width: this.settings.width + 'px',
			height: this.settings.height + 'px',
			display: 'block',
			'z-index': 0
		}),

		screen: $('<canvas/>', {
			id: 'youtyping-screen',
			'data-paper-keepalive': 'true',
			width: this.settings.width.toString(),
			height: this.settings.height.toString()
		}).appendTo(element).css({
			width: this.settings.width + 'px',
			height: this.settings.height + 'px',
			position: 'absolute',
			top: 0,
			left: 0,
			'z-index': 100
		})
	};

	// create YouTyping screen class
	this.screen = new Screen(document.getElementById('youtyping-screen'), this);

	// Initialize asynchronously
	// http://stackoverflow.com/questions/22346345/
	$.when(
		$.when(
			loadScoreXML(),
			$.Deferred(this.screen.setup).promise()
		).done(this.screen.load),
		setupPlayer()
	).done(this.screen.ready)
	.fail(function () {
		logTrace('ERROR: Initialization Failed...');
	});
};
<commit_msg>Expand variable<commit_after>var YouTyping = function (element, settings) {
	var youTyping = this;

	this.noteState = {
		WAITING: 0,
		HITTING: 1,
		CLEARED: 2,
		FAILED: 3
	};

	/******************* Internal functions *******************/

	var setupPlayerDeferred;
	var setupPlayer = function (callback) {
		setupPlayerDeferred = $.Deferred();
		logTrace('Setting Player Up...');

		var APITag = document.createElement('script');
		APITag.src = 'https://www.youtube.com/iframe_api';
		var firstScript = document.getElementsByTagName('script')[0];
		firstScript.parentNode.insertBefore(APITag, firstScript);

		return setupPlayerDeferred.promise();
	};

	// callback function used by YouTube IFrame Player API. must be global
	onYouTubeIframeAPIReady = function () {
		var settings = youTyping.settings;

		logTrace('Player API is Ready.');

		// try to hide advertisement if sandbox parameter is 'true' or not defined in URI query
		if (getParameterByName('sandbox') === 'true') {
			this.DOM.player.setAttribute('sandbox', 'allow-same-origin allow-scripts');
		}

		youTyping.player = new YT.Player('youtyping-player', {
			height: settings.height,
			width: settings.width,
			videoId: settings.videoId,
			playerVars: {
				rel: 0,
				// Wishing the best effort of hiding any information except for the video
				controls: 0,
				showinfo: 0,
				modestbranding: 1,
				wmode: 'opaque' // thanks http://stackoverflow.com/questions/6826386/
			},
			events: {
				'onReady': onPlayerReady,
				'onStateChange': onPlayerStateChange,
				'onError': onPlayerError
			}
		});
	};

	var onPlayerReady = function (event) {
		logTrace('Player is Ready.');
		setupPlayerDeferred.resolve();
	};

	var onPlayerStateChange = function (event) {
		switch (event.data) {
			case YT.PlayerState.ENDED:
				logTrace('Player Ended.');
				break;
			case YT.PlayerState.PLAYING:
				logTrace('Player Started.');
				break;
			case YT.PlayerState.PAUSED:
				logTrace('Player Paused.');
				break;
			case YT.PlayerState.BUFFERING:
				logTrace('Player Buffering.');
				break;
			case YT.PlayerState.CUED:
				logTrace('Player Cued.');
				break;
		}
	};

	var onPlayerError = function (event) {
		// from https://developers.google.com/youtube/iframe_api_reference
		switch (event.data) {
			case 2:
				logTrace('ERROR: The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.');
				break;
			case 5:
				logTrace('ERROR: The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.');
				break;
			case 100:
				logTrace('ERROR: The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.');
				break;
			case 101:
				logTrace('ERROR: The owner of the requested video does not allow it to be played in embedded players.');
				break;
			case 150:
				logTrace('ERROR: The owner of the requested video does not allow it to be played in embedded players.');
				break;
		}
		setupPlayerDeferred.reject();
	};

	var loadXMLDeferred;
	var loadScoreXML = function () {
		// Initialize deferred
		loadXMLDeferred = $.Deferred();

		$.ajax({
			url: youTyping.settings.score,
			type: 'get',
			datatype: 'xml',
			timeout: 1000,
			success: function (data, textStatus, jqXHR) {
				youTyping.scoreXML = $(data).find('fumen').find('item');
				logTrace('Loaded XML File.');

				// parse XML and store into YouTyping.score
				youTyping.score = [];

				$(youTyping.scoreXML).each(function () {
					var tempItem = {
						time: parseFloat($(this).attr('time')) * 1000, // convert to millisecond
						type: $(this).attr('type')
					};

					if ($(this).attr('text')) {
						tempItem.text = $(this).attr('text');
					}

					if (tempItem.type === '+') {
						tempItem.state = youTyping.noteState.WAITING;
					}

					youTyping.score.push(tempItem);
				});

				loadXMLDeferred.resolve();
			},
			error: function (jqXHR, textStatus, errorThrown) {
				logTrace('ERROR: XML File Loading Failed: ' + errorThrown);
				loadXMLDeferred.reject();
			}
		});

		return loadXMLDeferred.promise();
	};

	/******************* properties *******************/

	this.startTime = Date.now();

	// score data
	this.scoreXML = null;
	this.score = null;

	// YouTube Iframe Player
	this.player = null;

	// default settings
	this.settings = {
		zeroEstimateSamples: 16, // integer
		videoId: 'fQ_m5VLhqNg',
		score: 'data.utfx',
		width: 1120, // pixel
		height: 630, // pixel
		hitPosition: 200, // pixel
		noteSize: 50, // pixel
		speed: 0.5, // pixel per second
		scoreYpos: 0.5, // ratio
		longLineHeight: 150, // pixel
		lineHeight: 120, // pixel
		screenPadding: 30, // pixel
		judges: {
			perfect: {
				from: -30,
				to: 30
			},
			great: {
				from: -50,
				to: 50
			},
			good: {
				from: -70,
				to: 70
			}
		}
	};

	// ZeroTime calculation
	this.zeroTime = 0;
	this.zeroTimePad = 0;
	this.currentTime = 0;
	this.estimateSamples = [];
	this.estimatedZero = 0; // exposed only for debugging
	this.zeroCallFPS = 0; // exposed only for debugging

	// YouTyping.now
	Object.defineProperty(this, 'now', {
		get: function () {
			return window.performance.now();
		}
	});

	/******************* Methods *******************/

	this.play = function () {
		youTyping.player.playVideo();

		// Set interval to calculate `ZeroTime`

		/***************

		# What's `ZeroTime` and `ZeroCall`?

		The current time taken from YouTube API by `getCurrentTime()`
		is resoluted very roughly (about 0.2s) with a great range of errors (about 0.05s).

		It's so fatal for music game like YouTyping. So we introduced idea that calibrates
		correct playing time by taking average of measuring. That's `ZeroTime`.

		YouTyping loops to get current playing time from API (to `gotCurrentTime`)
		with enough interval time (10ms) to detect when the `getCurrentTime()` time jumped up to another value.
		And each time `gotCurrentTime` jumped (nameed `ZeroCall`),
		YouTyping assumes the time to be correct and counts backward to estimate when this video started,
		so the time is nameed `ZeroTime`. Then the current playing time of video will be calculated by `ZeroTime` and
		current time taken from browser clock (very highly resoluted as <1ms).

		***************/
		setInterval(function () {
			var gotCurrentTime = youTyping.player.getCurrentTime();
			var now = youTyping.now;

			if (gotCurrentTime === 0) { // if playing time is zero `ZeroTime` is immediately `now`!
				youTyping.zeroTimePad = now;
				youTyping.zeroTime = now;
			} else if (youTyping.currentTime !== gotCurrentTime) { // if Current Time jumped
				youTyping.currentTime = gotCurrentTime;
				youTyping.estimatedZero = now - youTyping.currentTime * 1000;

				// Estimated zero time is stored in estimatesamples and
				// we assume that correct zero time is average of recent
				// `zeroEstimateSamples` items of samples
				// because it contains great ranges of error.
				// We also introduced `zeroTimePad` to supress a sudden change of zeroTime.
				// It contains correct zero time and sudden-change-supressed zero time
				// will be stored in `zeroTime`.
				youTyping.estimateSamples.push(youTyping.estimatedZero);
				if (youTyping.estimateSamples.length > youTyping.settings.zeroEstimateSamples) {
					youTyping.estimateSamples.shift();
				}
				// just go hack :)
				var estimatedSum = youTyping.estimateSamples.reduce(function (previous, current) {
					return previous + current;
				});

				// `zeroTimePad` is actual estimated ZeroTime and real displayed ZeroTime is modested into `zeroTime`.
				youTyping.zeroTimePad = estimatedSum / youTyping.estimateSamples.length;

				youTyping.zeroCallFPS++;
			}
			youTyping.zeroTime = (youTyping.zeroTime - youTyping.zeroTimePad) * 0.9 + youTyping.zeroTimePad;
		}, 10);
	};

	// hit key
	// TODO: make HitEvent interface
	this.hit = function (key, time) {
		if (!time) {
			time = youTyping.now;
		}
		var absoluteTime = time - youTyping.zeroTime;

		// search for nearest note that matches currently passed key rule
		var nearestNote = null;
		var nearestDistance = Infinity;
		youTyping.score.forEach(function (item, index) {
			if (item.type === '+') {
				if (item.state === youTyping.noteState.WAITING && Math.abs(absoluteTime - item.time) < Math.abs(nearestDistance)) {
					nearestNote = item;
					nearestDistance = item.time - absoluteTime;
				}
			}
		});

		var distance = nearestDistance;

		if (nearestNote !== null) {
			var hitJudge = null;

			for (var judgeName in youTyping.settings.judges) {
				if (youTyping.settings.judges.hasOwnProperty(judgeName)) {
					var judge = youTyping.settings.judges[judgeName];
					if (judge.from <= distance && distance <= judge.to) {
						hitJudge = judgeName;
						break;
					}
				}
			}

			if (hitJudge !== null) {
				nearestNote.state = youTyping.noteState.CLEARED;
				console.log(distance, hitJudge);
			}
		}
	};

	/******************* Initialization *******************/

	// override default settings
	for (var param in settings) {
		if (settings.hasOwnProperty(param)) {
			if (this.settings[param] === undefined) {
				this.settings[param] = settings[param];
			} else if (typeof this.settings[param] === 'number') {
				this.settings[param] = parseInt(settings[param], 10);
			} else {
				this.settings[param] = settings[param];
			}
		}
	}

	// setup DOM
	/*
	 * div(this.DOM.wrap)
	 * |-div#youtyping-player(this.DOM.player)
	 * \-canvas#youtyping-screen(this.DOM.screen)
	 */
	this.DOM = {
		wrap: element.css({
			width: this.settings.width + 'px',
			height: this.settings.height + 'px',
			margin: '0 auto',
			position: 'relative'
		}),

		player: $('<div/>', {
			id: 'youtyping-player'
		}).appendTo(element).css({
			width: this.settings.width + 'px',
			height: this.settings.height + 'px',
			display: 'block',
			'z-index': 0
		}),

		screen: $('<canvas/>', {
			id: 'youtyping-screen',
			'data-paper-keepalive': 'true',
			width: this.settings.width.toString(),
			height: this.settings.height.toString()
		}).appendTo(element).css({
			width: this.settings.width + 'px',
			height: this.settings.height + 'px',
			position: 'absolute',
			top: 0,
			left: 0,
			'z-index': 100
		})
	};

	// create YouTyping screen class
	this.screen = new Screen(document.getElementById('youtyping-screen'), this);

	// Initialize asynchronously
	// http://stackoverflow.com/questions/22346345/
	$.when(
		$.when(
			loadScoreXML(),
			$.Deferred(this.screen.setup).promise()
		).done(this.screen.load),
		setupPlayer()
	).done(this.screen.ready)
	.fail(function () {
		logTrace('ERROR: Initialization Failed...');
	});
};
<|endoftext|>


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

    var moduleObj = moduler('load-more', {
        defaults: {
            url: null,
            event: 'click',
            contentElement: null, /* selector for element where content should be appended or replaced */
            page: 1, /* the page currently on */
            insertMode : 'append', /* append|replace */
            loadingCssClass: 'loading'
        },

        init: function (module) {
            if (!module.settings.url) {
                if (module.$element.is('a[href]')) {
                    module.settings.url = module.$element.attr('href');
                } else {
                    module.settings.url = window.location.href;
                }
            }

            // save a reference to contentElement
            module.$contentElement = module.settings.contentElement !== null ? $(module.settings.contentElement) : module.$element;

            module.$element.on(module.settings.event, module, moduleObj.listen.loadMore);
        },

        listen: {
            loadMore: mo.event(function (module, e) {
                e.preventDefault();

                // prevent additional requests when user spam-clicks
                if (module.isLoading) {
                    return;
                }

                module.$element.addClass(module.settings.loadingCssClass);

                // increase page by one
                module.settings.page += 1;
                module.isLoading = true;

                $.ajax({
                    type: 'GET',
                    url: module.settings.url.replace('{page}', module.settings.page),
                    data: {
                        partial: true,
                        page: module.settings.page
                    }
                })
                .always(function () {
                    module.$element.removeClass(module.settings.loadingCssClass);
                    module.isLoading = false;
                })
                .done(function (response, status, xhr) {
                    if (module.settings.insertMode == 'replace') {
                        module.$contentElement.html(response);
                    } else if (module.settings.insertMode == 'append') {
                        module.$contentElement.append(response);
                    }

                    if (xhr.getResponseHeader('X-LastPage')) {
                        // hide the load more button instead of removing it so that
                        // other modules can still trigger events
                        module.$element.hide();
                    }

                    module.$element.trigger('load-more-done', { response: response });
                })
                .error(function () {
                    module.$element.trigger('load-more-error');
                });
            })
        },

        destroy: function (module) {
            module.$element.off(module.settings.event, moduleObj.listen.loadMore);
        }
    });

})();<commit_msg>Added example HTML in load-more<commit_after>(function () {
    "use strict";

    /*
        Example HTML:

            Load more

    */

    var moduleObj = moduler('load-more', {
        defaults: {
            url: null,
            event: 'click',
            contentElement: null, /* selector for element where content should be appended or replaced */
            page: 1, /* the page currently on */
            insertMode : 'append', /* append|replace */
            loadingCssClass: 'loading'
        },

        init: function (module) {
            if (!module.settings.url) {
                if (module.$element.is('a[href]')) {
                    module.settings.url = module.$element.attr('href');
                } else {
                    module.settings.url = window.location.href;
                }
            }

            // save a reference to contentElement
            module.$contentElement = module.settings.contentElement !== null ? $(module.settings.contentElement) : module.$element;

            module.$element.on(module.settings.event, module, moduleObj.listen.loadMore);
        },

        listen: {
            loadMore: mo.event(function (module, e) {
                e.preventDefault();

                // prevent additional requests when user spam-clicks
                if (module.isLoading) {
                    return;
                }

                module.$element.addClass(module.settings.loadingCssClass);

                // increase page by one
                module.settings.page += 1;
                module.isLoading = true;

                $.ajax({
                    type: 'GET',
                    url: module.settings.url.replace('{page}', module.settings.page),
                    data: {
                        partial: true,
                        page: module.settings.page
                    }
                })
                .always(function () {
                    module.$element.removeClass(module.settings.loadingCssClass);
                    module.isLoading = false;
                })
                .done(function (response, status, xhr) {
                    if (module.settings.insertMode == 'replace') {
                        module.$contentElement.html(response);
                    } else if (module.settings.insertMode == 'append') {
                        module.$contentElement.append(response);
                    }

                    if (xhr.getResponseHeader('X-LastPage')) {
                        // hide the load more button instead of removing it so that
                        // other modules can still trigger events
                        module.$element.hide();
                    }

                    module.$element.trigger('load-more-done', { response: response });
                })
                .error(function () {
                    module.$element.trigger('load-more-error');
                });
            })
        },

        destroy: function (module) {
            module.$element.off(module.settings.event, moduleObj.listen.loadMore);
        }
    });

})();<|endoftext|>