<commit_before>import {BlockNode, TextblockNode, InlineNode, TextNode} from "./node"
import {StyleMarker} from "./style"

import {ProseMirrorError} from "../util/error"

export class SchemaError extends ProseMirrorError {}

function findKinds(type, name, schema, override) {
  function set(sub, sup) {
    if (sub in schema.kinds) {
      if (schema.kinds[sub] == sup) return
      SchemaError.raise(`Inconsistent superkinds for kind ${sub}: ${sup} and ${schema.kinds[sub]}`)
    }
    if (schema.subKind(sub, sup))
      SchemaError.raise(`Conflicting kind hierarchy through ${sub} and ${sup}`)
    schema.kinds[sub] = sup
  }

  for (let cur = type;; cur = Object.getPrototypeOf(cur)) {
    let curKind = override != null && cur == type ? override : cur.kind
    if (curKind != null) {
      let [_, kind, end] = /^(.*?)(\.)?$/.exec(curKind)
      if (kind) {
        set(name, kind)
        name = kind
      }
      if (end) {
        set(name, null)
        return
      }
    }
  }
}

export class NodeType {
  constructor(name, contains, attrs, schema) {
    this.name = name
    this.contains = contains
    this.attrs = attrs
    this.schema = schema
    this.defaultAttrs = null
    this.mustRecompute = false
    for (let attr in attrs)
      if (attrs[attr].mustRecompute) this.mustRecompute = true
  }

  get configurable() { return true }
  get isTextblock() { return false }
  get isBlock() { return false }
  get isInline() { return false }

  static get kind() { return "." }

  canContain(node) {
    return this.canContainType(node.type)
  }

  canContainType(type) {
    return this.schema.subKind(type.name, this.contains)
  }

  canContainChildren(node, liberal) {
    if (!liberal && !this.schema.subKind(node.type.contains, this.contains)) return false
    for (let i = 0; i < node.length; i++)
      if (!this.canContain(node.child(i))) return false
    return true
  }

  recomputeAttrs(attrs, content) {
    if (!this.mustRecompute) return attrs
    let copy = Object.create(null)
    for (let attr in attrs) {
      let desc = this.attrs[attr]
      copy[attr] = desc.mustRecompute ? desc.compute(this, content) : attrs[attr]
    }
    return copy
  }

  findConnection(other) {
    if (this.canContainType(other)) return []

    let seen = Object.create(null)
    let active = [{from: this, via: []}]
    while (active.length) {
      let current = active.shift()
      for (let name in this.schema.nodes) {
        let type = this.schema.nodeType(name)
        if (!(type.contains in seen) && current.from.canContainType(type)) {
          let via = current.via.concat(type)
          if (type.canContainType(other)) return via
          active.push({from: type, via: via})
          seen[type.contains] = true
        }
      }
    }
  }

  buildAttrs(attrs, content) {
    if (!attrs && this.defaultAttrs) return this.defaultAttrs
    else return buildAttrs(this.attrs, attrs, this, content)
  }

  create(attrs, content, styles) {
    return new this.instance(this, this.buildAttrs(attrs, content), content, styles)
  }

  static compile(types, schema) {
    let result = Object.create(null)
    for (let name in types) {
      let info = types[name]
      let type = info.type || SchemaError.raise("Missing node type for " + name)
      findKinds(type, name, schema, info.kind)
      let contains = "contains" in info ? info.contains : type.contains
      result[name] = new type(name, contains, info.attributes || type.attributes, schema)
    }
    for (let name in result) {
      let contains = result[name].contains
      if (contains && !(contains in schema.kinds))
        SchemaError.raise("Node type " + name + " is specified to contain non-existing kind " + contains)
    }
    if (!result.doc) SchemaError.raise("Every schema needs a 'doc' type")
    if (!result.text) SchemaError.raise("Every schema needs a 'text' type")

    for (let name in types)
      types[name].defaultAttrs = getDefaultAttrs(types[name].attrs)
    return result
  }

  static register(prop, value) {
    ;(this.prototype[prop] || (this.prototype[prop] = [])).push(value)
  }
}
NodeType.attributes = {}

export class Block extends NodeType {
  get instance() { return BlockNode }
  static get contains() { return "block" }
  static get kind() { return "block." }
  get isBlock() { return true }
}

export class Textblock extends Block {
  get instance() { return TextblockNode }
  static get contains() { return "inline" }
  get containsStyles() { return true }
  get isTextblock() { return true }

  canContain(node) {
    return super.canContain(node) && node.styles.every(s => this.canContainStyle(s))
  }

  canContainStyle(type) {
    let contains = this.containsStyles
    if (contains === true) return true
    if (contains) for (let i = 0; i < contains.length; i++)
      if (contains[i] == type.name) return true
    return false
  }
}

export class Inline extends NodeType {
  get instance() { return InlineNode }
  static get contains() { return null }
  static get kind() { return "inline." }
  get isInline() { return true }
}

export class Text extends Inline {
  get instance() { return TextNode }
}

// Attribute descriptors

export class Attribute {
  constructor(options = {}) {
    this.default = options.default
    this.compute = options.compute
    this.mustRecompute = options.mustRecompute
    this.inheritable = options.inheritable
  }
}

// Styles

export class StyleType {
  constructor(name, attrs, rank, schema) {
    this.name = name
    this.attrs = attrs
    this.rank = rank
    this.schema = schema
    let defaults = getDefaultAttrs(this.attrs)
    this.instance = defaults && new StyleMarker(this, defaults)
  }

  static get rank() { return 50 }

  create(attrs) {
    if (!attrs && this.instance) return this.instance
    return new StyleMarker(this, buildAttrs(this.attrs, attrs, this))
  }

  static getOrder(styles) {
    let sorted = []
    for (let name in styles) sorted.push({name, rank: styles[name].type.rank})
    sorted.sort((a, b) => a.rank - b.rank)
    let ranks = Object.create(null)
    for (let i = 0; i < sorted.length; i++) ranks[sorted[i].name] = i
    return ranks
  }

  static compile(styles, schema) {
    let order = this.getOrder(styles)
    let result = Object.create(null)
    for (let name in styles) {
      let info = styles[name]
      let attrs = info.attributes || info.type.attributes
      result[name] = new info.type(name, attrs, order[name], schema)
    }
    return result
  }

  static register(prop, value) {
    ;(this.prototype[prop] || (this.prototype[prop] = [])).push(value)
  }
}
StyleType.attributes = {}

// Schema specifications are data structures that specify a schema --
// a set of node types, their names, attributes, and nesting behavior.

function copyObj(obj, f) {
  let result = Object.create(null)
  for (let prop in obj) result[prop] = f ? f(obj[prop]) : obj[prop]
  return result
}

function ensureWrapped(obj) {
  return obj instanceof Function ? {type: obj} : obj
}

function overlayObj(obj, overlay) {
  let copy = copyObj(obj)
  for (let name in overlay) {
    let info = ensureWrapped(overlay[name])
    if (info == null) {
      delete copy[name]
    } else if (info.type) {
      copy[name] = info
    } else {
      let existing = copy[name] = copyObj(copy[name])
      for (let prop in info)
        existing[prop] = info[prop]
    }
  }
  return copy
}

export class SchemaSpec {
  constructor(nodes, styles) {
    this.nodes = nodes ? copyObj(nodes, ensureWrapped) : Object.create(null)
    this.styles = styles ? copyObj(styles, ensureWrapped) : Object.create(null)
  }

  updateNodes(nodes) {
    return new SchemaSpec(overlayObj(this.nodes, nodes), this.styles)
  }

  addAttribute(filter, attrName, attrInfo) {
    let copy = copyObj(this.nodes)
    for (let name in copy) {
      if (typeof filter == "string" ? filter == name :
          typeof filter == "function" ? filter(name, copy[name]) :
          filter ? filter == copy[name] : true) {
        let info = copy[name] = copyObj(copy[name])
        if (!info.attributes) info.attributes = copyObj(info.type.attributes)
        info.attributes[attrName] = attrInfo
      }
    }
    return new SchemaSpec(copy, this.styles)
  }

  updateStyles(styles) {
    return new SchemaSpec(this.nodes, overlayObj(this.styles, styles))
  }
}

// For node types where all attrs have a default value (or which don't
// have any attributes), build up a single reusable default attribute
// object, and use it for all nodes that don't specify specific
// attributes.

function getDefaultAttrs(attrs) {
  let defaults = Object.create(null)
  for (let attrName in attrs) {
    let attr = attrs[attrName]
    if (attr.default == null) return null
    defaults[attrName] = attr.default
  }
  return defaults
}

function buildAttrs(attrSpec, attrs, arg1, arg2) {
  let built = Object.create(null)
  for (let name in attrSpec) {
    let value = attrs && attrs[name]
    if (value == null) {
      let attr = attrSpec[name]
      if (attr.default != null)
        value = attr.default
      else if (attr.compute)
        value = attr.compute(arg1, arg2)
      else
        SchemaError.raise("No value supplied for attribute " + name)
    }
    built[name] = value
  }
  return built
}

/**
 * Document schema class.
 */
export class Schema {
  constructor(spec) {
    this.spec = spec
    this.kinds = Object.create(null)
    this.nodes = NodeType.compile(spec.nodes, this)
    this.styles = StyleType.compile(spec.styles, this)
    this.cached = Object.create(null)

    this.node = this.node.bind(this)
    this.text = this.text.bind(this)
    this.nodeFromJSON = this.nodeFromJSON.bind(this)
    this.styleFromJSON = this.styleFromJSON.bind(this)
  }

  node(type, attrs, content, styles) {
    if (typeof type == "string")
      type = this.nodeType(type)
    else if (!(type instanceof NodeType))
      SchemaError.raise("Invalid node type: " + type)
    else if (type.schema != this)
      SchemaError.raise("Node type from different schema used (" + type.name + ")")

    return type.create(attrs, content, styles)
  }

  text(text, styles) {
    return this.nodes.text.create(null, text, styles)
  }

  defaultTextblockType() {
    let cached = this.cached.defaultTextblockType
    if (cached !== undefined) return cached
    for (let name in this.nodes) {
      if (this.nodes[name].defaultTextblock)
        return this.cached.defaultTextblockType = this.nodes[name]
    }
    return this.cached.defaultTextblockType = null
  }

  style(name, attrs) {
    let spec = this.styles[name] || SchemaError.raise("No style named " + name)
    return spec.create(attrs)
  }

  nodeFromJSON(json) {
    let type = this.nodeType(json.type)
    return type.create(json.attrs,
                       json.text || (json.content && json.content.map(this.nodeFromJSON)),
                       json.styles && json.styles.map(this.styleFromJSON))
  }

  styleFromJSON(json) {
    if (typeof json == "string") return this.style(json)
    return this.style(json._, json)
  }

  nodeType(name) {
    return this.nodes[name] || SchemaError.raise("Unknown node type: " + name)
  }

  subKind(sub, sup) {
    for (;;) {
      if (sub == sup) return true
      sub = this.kinds[sub]
      if (!sub) return false
    }
  }
}
<commit_msg>Allow directly passing nodes and styles to Schema constructor<commit_after>import {BlockNode, TextblockNode, InlineNode, TextNode} from "./node"
import {StyleMarker} from "./style"

import {ProseMirrorError} from "../util/error"

export class SchemaError extends ProseMirrorError {}

function findKinds(type, name, schema, override) {
  function set(sub, sup) {
    if (sub in schema.kinds) {
      if (schema.kinds[sub] == sup) return
      SchemaError.raise(`Inconsistent superkinds for kind ${sub}: ${sup} and ${schema.kinds[sub]}`)
    }
    if (schema.subKind(sub, sup))
      SchemaError.raise(`Conflicting kind hierarchy through ${sub} and ${sup}`)
    schema.kinds[sub] = sup
  }

  for (let cur = type;; cur = Object.getPrototypeOf(cur)) {
    let curKind = override != null && cur == type ? override : cur.kind
    if (curKind != null) {
      let [_, kind, end] = /^(.*?)(\.)?$/.exec(curKind)
      if (kind) {
        set(name, kind)
        name = kind
      }
      if (end) {
        set(name, null)
        return
      }
    }
  }
}

export class NodeType {
  constructor(name, contains, attrs, schema) {
    this.name = name
    this.contains = contains
    this.attrs = attrs
    this.schema = schema
    this.defaultAttrs = null
    this.mustRecompute = false
    for (let attr in attrs)
      if (attrs[attr].mustRecompute) this.mustRecompute = true
  }

  get configurable() { return true }
  get isTextblock() { return false }
  get isBlock() { return false }
  get isInline() { return false }

  static get kind() { return "." }

  canContain(node) {
    return this.canContainType(node.type)
  }

  canContainType(type) {
    return this.schema.subKind(type.name, this.contains)
  }

  canContainChildren(node, liberal) {
    if (!liberal && !this.schema.subKind(node.type.contains, this.contains)) return false
    for (let i = 0; i < node.length; i++)
      if (!this.canContain(node.child(i))) return false
    return true
  }

  recomputeAttrs(attrs, content) {
    if (!this.mustRecompute) return attrs
    let copy = Object.create(null)
    for (let attr in attrs) {
      let desc = this.attrs[attr]
      copy[attr] = desc.mustRecompute ? desc.compute(this, content) : attrs[attr]
    }
    return copy
  }

  findConnection(other) {
    if (this.canContainType(other)) return []

    let seen = Object.create(null)
    let active = [{from: this, via: []}]
    while (active.length) {
      let current = active.shift()
      for (let name in this.schema.nodes) {
        let type = this.schema.nodeType(name)
        if (!(type.contains in seen) && current.from.canContainType(type)) {
          let via = current.via.concat(type)
          if (type.canContainType(other)) return via
          active.push({from: type, via: via})
          seen[type.contains] = true
        }
      }
    }
  }

  buildAttrs(attrs, content) {
    if (!attrs && this.defaultAttrs) return this.defaultAttrs
    else return buildAttrs(this.attrs, attrs, this, content)
  }

  create(attrs, content, styles) {
    return new this.instance(this, this.buildAttrs(attrs, content), content, styles)
  }

  static compile(types, schema) {
    let result = Object.create(null)
    for (let name in types) {
      let info = types[name]
      let type = info.type || SchemaError.raise("Missing node type for " + name)
      findKinds(type, name, schema, info.kind)
      let contains = "contains" in info ? info.contains : type.contains
      result[name] = new type(name, contains, info.attributes || type.attributes, schema)
    }
    for (let name in result) {
      let contains = result[name].contains
      if (contains && !(contains in schema.kinds))
        SchemaError.raise("Node type " + name + " is specified to contain non-existing kind " + contains)
    }
    if (!result.doc) SchemaError.raise("Every schema needs a 'doc' type")
    if (!result.text) SchemaError.raise("Every schema needs a 'text' type")

    for (let name in types)
      types[name].defaultAttrs = getDefaultAttrs(types[name].attrs)
    return result
  }

  static register(prop, value) {
    ;(this.prototype[prop] || (this.prototype[prop] = [])).push(value)
  }
}
NodeType.attributes = {}

export class Block extends NodeType {
  get instance() { return BlockNode }
  static get contains() { return "block" }
  static get kind() { return "block." }
  get isBlock() { return true }
}

export class Textblock extends Block {
  get instance() { return TextblockNode }
  static get contains() { return "inline" }
  get containsStyles() { return true }
  get isTextblock() { return true }

  canContain(node) {
    return super.canContain(node) && node.styles.every(s => this.canContainStyle(s))
  }

  canContainStyle(type) {
    let contains = this.containsStyles
    if (contains === true) return true
    if (contains) for (let i = 0; i < contains.length; i++)
      if (contains[i] == type.name) return true
    return false
  }
}

export class Inline extends NodeType {
  get instance() { return InlineNode }
  static get contains() { return null }
  static get kind() { return "inline." }
  get isInline() { return true }
}

export class Text extends Inline {
  get instance() { return TextNode }
}

// Attribute descriptors

export class Attribute {
  constructor(options = {}) {
    this.default = options.default
    this.compute = options.compute
    this.mustRecompute = options.mustRecompute
    this.inheritable = options.inheritable
  }
}

// Styles

export class StyleType {
  constructor(name, attrs, rank, schema) {
    this.name = name
    this.attrs = attrs
    this.rank = rank
    this.schema = schema
    let defaults = getDefaultAttrs(this.attrs)
    this.instance = defaults && new StyleMarker(this, defaults)
  }

  static get rank() { return 50 }

  create(attrs) {
    if (!attrs && this.instance) return this.instance
    return new StyleMarker(this, buildAttrs(this.attrs, attrs, this))
  }

  static getOrder(styles) {
    let sorted = []
    for (let name in styles) sorted.push({name, rank: styles[name].type.rank})
    sorted.sort((a, b) => a.rank - b.rank)
    let ranks = Object.create(null)
    for (let i = 0; i < sorted.length; i++) ranks[sorted[i].name] = i
    return ranks
  }

  static compile(styles, schema) {
    let order = this.getOrder(styles)
    let result = Object.create(null)
    for (let name in styles) {
      let info = styles[name]
      let attrs = info.attributes || info.type.attributes
      result[name] = new info.type(name, attrs, order[name], schema)
    }
    return result
  }

  static register(prop, value) {
    ;(this.prototype[prop] || (this.prototype[prop] = [])).push(value)
  }
}
StyleType.attributes = {}

// Schema specifications are data structures that specify a schema --
// a set of node types, their names, attributes, and nesting behavior.

function copyObj(obj, f) {
  let result = Object.create(null)
  for (let prop in obj) result[prop] = f ? f(obj[prop]) : obj[prop]
  return result
}

function ensureWrapped(obj) {
  return obj instanceof Function ? {type: obj} : obj
}

function overlayObj(obj, overlay) {
  let copy = copyObj(obj)
  for (let name in overlay) {
    let info = ensureWrapped(overlay[name])
    if (info == null) {
      delete copy[name]
    } else if (info.type) {
      copy[name] = info
    } else {
      let existing = copy[name] = copyObj(copy[name])
      for (let prop in info)
        existing[prop] = info[prop]
    }
  }
  return copy
}

export class SchemaSpec {
  constructor(nodes, styles) {
    this.nodes = nodes ? copyObj(nodes, ensureWrapped) : Object.create(null)
    this.styles = styles ? copyObj(styles, ensureWrapped) : Object.create(null)
  }

  updateNodes(nodes) {
    return new SchemaSpec(overlayObj(this.nodes, nodes), this.styles)
  }

  addAttribute(filter, attrName, attrInfo) {
    let copy = copyObj(this.nodes)
    for (let name in copy) {
      if (typeof filter == "string" ? filter == name :
          typeof filter == "function" ? filter(name, copy[name]) :
          filter ? filter == copy[name] : true) {
        let info = copy[name] = copyObj(copy[name])
        if (!info.attributes) info.attributes = copyObj(info.type.attributes)
        info.attributes[attrName] = attrInfo
      }
    }
    return new SchemaSpec(copy, this.styles)
  }

  updateStyles(styles) {
    return new SchemaSpec(this.nodes, overlayObj(this.styles, styles))
  }
}

// For node types where all attrs have a default value (or which don't
// have any attributes), build up a single reusable default attribute
// object, and use it for all nodes that don't specify specific
// attributes.

function getDefaultAttrs(attrs) {
  let defaults = Object.create(null)
  for (let attrName in attrs) {
    let attr = attrs[attrName]
    if (attr.default == null) return null
    defaults[attrName] = attr.default
  }
  return defaults
}

function buildAttrs(attrSpec, attrs, arg1, arg2) {
  let built = Object.create(null)
  for (let name in attrSpec) {
    let value = attrs && attrs[name]
    if (value == null) {
      let attr = attrSpec[name]
      if (attr.default != null)
        value = attr.default
      else if (attr.compute)
        value = attr.compute(arg1, arg2)
      else
        SchemaError.raise("No value supplied for attribute " + name)
    }
    built[name] = value
  }
  return built
}

/**
 * Document schema class.
 */
export class Schema {
  constructor(spec, styles) {
    if (!(spec instanceof SchemaSpec)) spec = new SchemaSpec(spec, styles)
    this.spec = spec
    this.kinds = Object.create(null)
    this.nodes = NodeType.compile(spec.nodes, this)
    this.styles = StyleType.compile(spec.styles, this)
    this.cached = Object.create(null)

    this.node = this.node.bind(this)
    this.text = this.text.bind(this)
    this.nodeFromJSON = this.nodeFromJSON.bind(this)
    this.styleFromJSON = this.styleFromJSON.bind(this)
  }

  node(type, attrs, content, styles) {
    if (typeof type == "string")
      type = this.nodeType(type)
    else if (!(type instanceof NodeType))
      SchemaError.raise("Invalid node type: " + type)
    else if (type.schema != this)
      SchemaError.raise("Node type from different schema used (" + type.name + ")")

    return type.create(attrs, content, styles)
  }

  text(text, styles) {
    return this.nodes.text.create(null, text, styles)
  }

  defaultTextblockType() {
    let cached = this.cached.defaultTextblockType
    if (cached !== undefined) return cached
    for (let name in this.nodes) {
      if (this.nodes[name].defaultTextblock)
        return this.cached.defaultTextblockType = this.nodes[name]
    }
    return this.cached.defaultTextblockType = null
  }

  style(name, attrs) {
    let spec = this.styles[name] || SchemaError.raise("No style named " + name)
    return spec.create(attrs)
  }

  nodeFromJSON(json) {
    let type = this.nodeType(json.type)
    return type.create(json.attrs,
                       json.text || (json.content && json.content.map(this.nodeFromJSON)),
                       json.styles && json.styles.map(this.styleFromJSON))
  }

  styleFromJSON(json) {
    if (typeof json == "string") return this.style(json)
    return this.style(json._, json)
  }

  nodeType(name) {
    return this.nodes[name] || SchemaError.raise("Unknown node type: " + name)
  }

  subKind(sub, sup) {
    for (;;) {
      if (sub == sup) return true
      sub = this.kinds[sub]
      if (!sub) return false
    }
  }
}
<|endoftext|>


<commit_before>
import CorePlugin from '../plugins/core'
import Debug from 'debug'
import Schema from './schema'
import State from './state'
import { Record } from 'immutable'

/**
 * Debug.
 *
 * @type {Function}
 */

const debug = Debug('slate:stack')

/**
 * Methods that are triggered on events and can change the state.
 *
 * @type {Array}
 */

const EVENT_HANDLER_METHODS = [
  'onBeforeInput',
  'onBlur',
  'onFocus',
  'onCopy',
  'onCut',
  'onDrop',
  'onKeyDown',
  'onPaste',
  'onSelect',
]

/**
 * Methods that accumulate an updated state.
 *
 * @type {Array}
 */

const STATE_ACCUMULATOR_METHODS = [
  'onBeforeChange',
  'onChange',
]

/**
 * Default properties.
 *
 * @type {Object}
 */

const DEFAULTS = {
  plugins: [],
  schema: new Schema(),
}

/**
 * Stack.
 *
 * @type {Stack}
 */

class Stack extends new Record(DEFAULTS) {

  /**
   * Constructor.
   *
   * @param {Object} properties
   *   @property {Array} plugins
   *   @property {Schema|Object} schema
   *   @property {Function} ...handlers
   */

  static create(properties) {
    const plugins = resolvePlugins(properties)
    const schema = resolveSchema(plugins)
    return new Stack({ plugins, schema })
  }

  /**
   * Get the kind.
   *
   * @return {String}
   */

  get kind() {
    return 'stack'
  }

  /**
   * Invoke `render` on all of the plugins in reverse, building up a tree of
   * higher-order components.
   *
   * @param {State} state
   * @param {Editor} editor
   * @param {Object} children
   * @param {Object} props
   * @return {Component}
   */

  render = (state, editor, props) => {
    debug('render')
    const plugins = this.plugins.slice().reverse()
    let children

    for (const plugin of plugins) {
      if (!plugin.render) continue
      children = plugin.render(props, state, editor)
      props.children = children
    }

    return children
  }

  /**
   * Invoke `renderPortal` on all of the plugins, building a list of portals.
   *
   * @param {State} state
   * @param {Editor} editor
   * @return {Array}
   */

  renderPortal = (state, editor) => {
    debug('renderPortal')
    const portals = []

    for (const plugin of this.plugins) {
      if (!plugin.renderPortal) continue
      const portal = plugin.renderPortal(state, editor)
      if (portal == null) continue
      portals.push(portal)
    }

    return portals
  }

}

/**
 * Mix in the event handler methods.
 *
 * @param {State} state
 * @param {Editor} editor
 * @param {Mixed} ...args
 * @return {State|Null}
 */

for (const method of EVENT_HANDLER_METHODS) {
  Stack.prototype[method] = function (state, editor, ...args) {
    debug(method)

    if (method == 'onChange') {
      state = this.onBeforeChange(state, editor)
    }

    for (const plugin of this.plugins) {
      if (!plugin[method]) continue
      const next = plugin[method](...args, state, editor)
      if (next == null) continue
      assertState(next)
      return next
    }

    return state
  }
}

/**
 * Mix in the state accumulator methods.
 *
 * @param {State} state
 * @param {Editor} editor
 * @param {Mixed} ...args
 * @return {State|Null}
 */

for (const method of STATE_ACCUMULATOR_METHODS) {
  Stack.prototype[method] = function (state, editor, ...args) {
    debug(method)

    for (const plugin of this.plugins) {
      if (!plugin[method]) continue
      const next = plugin[method](...args, state, editor)
      if (next == null) continue
      assertState(next)
      state = next
    }

    return state
  }
}

/**
 * Assert that a `value` is a state object.
 *
 * @param {Mixed} value
 */

function assertState(value) {
  if (value instanceof State) return
  throw new Error(`A plugin returned an unexpected state value: ${value}`)
}

/**
 * Resolve a schema from a set of `plugins`.
 *
 * @param {Array} plugins
 * @return {Schema}
 */

function resolveSchema(plugins) {
  let rules = []

  for (const plugin of plugins) {
    if (plugin.schema == null) continue
    const schema = Schema.create(plugin.schema)
    rules = rules.concat(schema.rules)
  }

  const schema = Schema.create({ rules })
  return schema
}

/**
 * Resolve an array of plugins from `properties`.
 *
 * In addition to the plugins provided in `properties.plugins`, this will
 * create two other plugins:
 *
 * - A plugin made from the top-level `properties` themselves, which are
 * placed at the beginning of the stack. That way, you can add a `onKeyDown`
 * handler, and it will override all of the existing plugins.
 *
 * - A "core" functionality plugin that handles the most basic events in Slate,
 * like deleting characters, splitting blocks, etc.
 *
 * @param {Object} props
 * @return {Array}
 */

function resolvePlugins(props) {
  const { plugins = [], ...overridePlugin } = props
  const corePlugin = CorePlugin(props)
  return [
    overridePlugin,
    ...plugins,
    corePlugin
  ]
}

/**
 * Export.
 *
 * @type {Stack}
 */

export default Stack
<commit_msg>Move condition about onBeforeChange (#878)<commit_after>
import CorePlugin from '../plugins/core'
import Debug from 'debug'
import Schema from './schema'
import State from './state'
import { Record } from 'immutable'

/**
 * Debug.
 *
 * @type {Function}
 */

const debug = Debug('slate:stack')

/**
 * Methods that are triggered on events and can change the state.
 *
 * @type {Array}
 */

const EVENT_HANDLER_METHODS = [
  'onBeforeInput',
  'onBlur',
  'onFocus',
  'onCopy',
  'onCut',
  'onDrop',
  'onKeyDown',
  'onPaste',
  'onSelect',
]

/**
 * Methods that accumulate an updated state.
 *
 * @type {Array}
 */

const STATE_ACCUMULATOR_METHODS = [
  'onBeforeChange',
  'onChange',
]

/**
 * Default properties.
 *
 * @type {Object}
 */

const DEFAULTS = {
  plugins: [],
  schema: new Schema(),
}

/**
 * Stack.
 *
 * @type {Stack}
 */

class Stack extends new Record(DEFAULTS) {

  /**
   * Constructor.
   *
   * @param {Object} properties
   *   @property {Array} plugins
   *   @property {Schema|Object} schema
   *   @property {Function} ...handlers
   */

  static create(properties) {
    const plugins = resolvePlugins(properties)
    const schema = resolveSchema(plugins)
    return new Stack({ plugins, schema })
  }

  /**
   * Get the kind.
   *
   * @return {String}
   */

  get kind() {
    return 'stack'
  }

  /**
   * Invoke `render` on all of the plugins in reverse, building up a tree of
   * higher-order components.
   *
   * @param {State} state
   * @param {Editor} editor
   * @param {Object} children
   * @param {Object} props
   * @return {Component}
   */

  render = (state, editor, props) => {
    debug('render')
    const plugins = this.plugins.slice().reverse()
    let children

    for (const plugin of plugins) {
      if (!plugin.render) continue
      children = plugin.render(props, state, editor)
      props.children = children
    }

    return children
  }

  /**
   * Invoke `renderPortal` on all of the plugins, building a list of portals.
   *
   * @param {State} state
   * @param {Editor} editor
   * @return {Array}
   */

  renderPortal = (state, editor) => {
    debug('renderPortal')
    const portals = []

    for (const plugin of this.plugins) {
      if (!plugin.renderPortal) continue
      const portal = plugin.renderPortal(state, editor)
      if (portal == null) continue
      portals.push(portal)
    }

    return portals
  }

}

/**
 * Mix in the event handler methods.
 *
 * @param {State} state
 * @param {Editor} editor
 * @param {Mixed} ...args
 * @return {State|Null}
 */

for (const method of EVENT_HANDLER_METHODS) {
  Stack.prototype[method] = function (state, editor, ...args) {
    debug(method)

    for (const plugin of this.plugins) {
      if (!plugin[method]) continue
      const next = plugin[method](...args, state, editor)
      if (next == null) continue
      assertState(next)
      return next
    }

    return state
  }
}

/**
 * Mix in the state accumulator methods.
 *
 * @param {State} state
 * @param {Editor} editor
 * @param {Mixed} ...args
 * @return {State|Null}
 */

for (const method of STATE_ACCUMULATOR_METHODS) {
  Stack.prototype[method] = function (state, editor, ...args) {
    debug(method)

    if (method == 'onChange') {
      state = this.onBeforeChange(state, editor)
    }

    for (const plugin of this.plugins) {
      if (!plugin[method]) continue
      const next = plugin[method](...args, state, editor)
      if (next == null) continue
      assertState(next)
      state = next
    }

    return state
  }
}

/**
 * Assert that a `value` is a state object.
 *
 * @param {Mixed} value
 */

function assertState(value) {
  if (value instanceof State) return
  throw new Error(`A plugin returned an unexpected state value: ${value}`)
}

/**
 * Resolve a schema from a set of `plugins`.
 *
 * @param {Array} plugins
 * @return {Schema}
 */

function resolveSchema(plugins) {
  let rules = []

  for (const plugin of plugins) {
    if (plugin.schema == null) continue
    const schema = Schema.create(plugin.schema)
    rules = rules.concat(schema.rules)
  }

  const schema = Schema.create({ rules })
  return schema
}

/**
 * Resolve an array of plugins from `properties`.
 *
 * In addition to the plugins provided in `properties.plugins`, this will
 * create two other plugins:
 *
 * - A plugin made from the top-level `properties` themselves, which are
 * placed at the beginning of the stack. That way, you can add a `onKeyDown`
 * handler, and it will override all of the existing plugins.
 *
 * - A "core" functionality plugin that handles the most basic events in Slate,
 * like deleting characters, splitting blocks, etc.
 *
 * @param {Object} props
 * @return {Array}
 */

function resolvePlugins(props) {
  const { plugins = [], ...overridePlugin } = props
  const corePlugin = CorePlugin(props)
  return [
    overridePlugin,
    ...plugins,
    corePlugin
  ]
}

/**
 * Export.
 *
 * @type {Stack}
 */

export default Stack
<|endoftext|>


<commit_before>/**
 * Titon: The Mootools UI Framework
 *
 * @copyright	Copyright 2006-2012, Titon
 * @license		http://opensource.org/licenses/mit-license.php - Licensed under the MIT License
 * @link		http://github.com/titon
 */

/**
 * Provides tabbed support to an element containing navigation tabs and sections.
 * Each time a tab is clicked, the section with the same ID as the tab href will be displayed.
 *
 * {{{
 *

 *
 *

 *
Tab 1

 *
Tab 2

 *

 *
 *
 *

 *

 *

 *
 *
 * }}}
 *
 * @version	1.0.0
 * @uses	Titon
 * @uses	Titon.Module
 * @uses	Core
 */
Titon.Tabs = new Class({
	Extends: Titon.Module,

	/**
	 * Collection of content sections.
	 */
	sections: [],

	/**
	 * Collection of tabs (anchor links).
	 */
	tabs: [],

	/**
	 * The current and previous shown indices.
	 */
	previousIndex: 0,
	currentIndex: 0,

	/**
	 * Default options.
	 *
	 *	ajax			- (boolean) Will load the href as an ajax call when applicable
	 *	fade			- (boolean) Should the sections fade in
	 *	fadeDuration	- (int) Fade duration in milliseconds
	 *	mode			- (string) Either "hover" or "click"
	 *	activeClass		- (string) Class name appended to the active tab
	 *	defaultIndex	- (int) Index of the tab/section to display by default
	 *	persistState	- (boolean) Will persist the last tab clicked between page loads
	 *	cookie			- (string) The key used in the cookie name
	 *	cookieDuration	- (int) The length the cookie will last (in days)
	 *	errorMessage	- (string) Error message when AJAX calls fail
	 *	loadingMessage	- (string) Loading message while waiting for AJAX calls
	 *	tabsElement		- (string) The CSS query to grab the tab elements
	 *	sectionsElement	- (string) The CSS query to grab the section elements
	 *	template		- (string) Do not use an HTML template
	 *	onShow			- (function) Callback to trigger when a section is shown
	 */
	options: {
		ajax: true,
		fade: false,
		fadeDuration: 600,
		mode: 'click',
		activeClass: Titon.options.activeClass,
		defaultIndex: 0,
		persistState: false,
		cookie: null,
		cookieDuration: 30,
		errorMessage: Titon.msg.error,
		loadingMessage: Titon.msg.loading,
		tabsElement: 'nav a',
		sectionsElement: 'section',
		template: false,

		// Events
		onShow: null
	},

	/**
	 * Initialize Tabs by storing the query, gathering the elements and binding events.
	 *
	 * @param {String} query
	 * @param {Object} options
	 */
	initialize: function(query, options) {
		options.cookie = (options.cookie || query).camelCase();
		options.templateFrom = query;

		this.parent(options);
		this.query = query;

		// Get elements
		this.tabs = this.element.getElements(this.options.tabsElement);
		this.tabs.each(function(tab, index) {
			tab.set('data-tabs-index', index).removeClass(this.options.activeClass);
		}.bind(this));

		this.sections = this.element.getElements(this.options.sectionsElement);
		this.sections.hide();

		// Set events
		this.tabs.addEvent((this.options.mode === 'click') ? 'click' : 'mouseover', this._listen.bind(this));

		// Trigger default tab to display
		var index = Number.from(Cookie.read('titon.tabs.' + this.options.cookie) || this.options.defaultIndex);

		this.show(index);
	},

	/**
	 * Show the content based on the tab. Can either pass an integer as the index in the collection,
	 * or pass an element object for a tab in the collection.
	 *
	 * @param {Element|int} tab
	 */
	show: function(tab) {
		if (typeOf(tab) === 'number') {
			tab = this.tabs[tab] || null;
		}

		if (!tab) {
			return;
		}

		var className = this.options.activeClass,
			target = tab.get('data-tabs-index'),
			url = tab.get('href'),
			section = this.sections[target];

		// Load content with AJAX
		if (url && !url.contains('#') && this.options.ajax) {
			if (!this.cache[url]) {
				new Request({
					url: url,
					method: 'get',
					evalScripts: true,
					onSuccess: function(response) {
						this.cache[url] = response;
						section.setHtml(response);
					}.bind(this),
					onRequest: function() {
						if (this.options.showLoading) {
							section.setHtml(new Element('div.tabs-loading', {
								text: this.options.loadingMessage
							}));
						}
					}.bind(this),
					onFailure: function() {
						section.setHtml(new Element('div.tabs-error', {
							text: this.options.errorMessage
						}));
					}.bind(this)
				}).get();
			}
		}

		// Toggle tabs
		this.tabs.removeClass(className);

		tab.addClass(className);

		// Toggle sections
		this.sections.hide();

		if (this.options.fade) {
			section.fadeIn(this.options.fadeDuration)
		} else {
			section.show();
		}

		// Persist the state using a cookie
		if (this.options.persistState) {
			Cookie.write('titon.tabs.' + this.options.cookie, tab.get('data-tabs-index'), {
				duration: this.options.cookieDuration
			});
		}

		// Track
		this.previousIndex = this.currentIndex;
		this.currentIndex = tab.get('data-tabs-index');

		this.fireEvent('show', tab);
	},

	/**
	 * Event callback for tab element click.
	 *
	 * @param {Event} e
	 */
	_listen: function(e) {
		e.stop();

		this.show(e.target);
	}

});

/**
 * All instances loaded via factory().
 */
Titon.Tabs.instances = {};

/**
 * Easily create multiple instances.
 *
 * @param {String} query
 * @param {Object} options
 * @return {Titon.Tabs}
 */
Titon.Tabs.factory = function(query, options) {
	if (Titon.Tabs.instances[query]) {
		return Titon.Tabs.instances[query];
	}

	var instance = new Titon.Tabs(query, options);

	Titon.Tabs.instances[query] = instance;

	return instance;
};<commit_msg>Added enable() and disable() to Tabs<commit_after>/**
 * Titon: The Mootools UI Framework
 *
 * @copyright	Copyright 2006-2012, Titon
 * @license		http://opensource.org/licenses/mit-license.php - Licensed under the MIT License
 * @link		http://github.com/titon
 */

/**
 * Provides tabbed support to an element containing navigation tabs and sections.
 * Each time a tab is clicked, the section with the same ID as the tab href will be displayed.
 *
 * {{{
 *

 *
 *

 *
Tab 1

 *
Tab 2

 *

 *
 *
 *

 *

 *

 *
 *
 * }}}
 *
 * @version	1.0.0
 * @uses	Titon
 * @uses	Titon.Module
 * @uses	Core
 * @uses	More/Class.Binds
 */
Titon.Tabs = new Class({
	Extends: Titon.Module,
	Binds: ['_listen'],

	/**
	 * Collection of content sections.
	 */
	sections: [],

	/**
	 * Collection of tabs (anchor links).
	 */
	tabs: [],

	/**
	 * The current and previous shown indices.
	 */
	previousIndex: 0,
	currentIndex: 0,

	/**
	 * Default options.
	 *
	 *	ajax			- (boolean) Will load the href as an ajax call when applicable
	 *	fade			- (boolean) Should the sections fade in
	 *	fadeDuration	- (int) Fade duration in milliseconds
	 *	mode			- (string) Either "hover" or "click"
	 *	activeClass		- (string) Class name appended to the active tab
	 *	defaultIndex	- (int) Index of the tab/section to display by default
	 *	persistState	- (boolean) Will persist the last tab clicked between page loads
	 *	cookie			- (string) The key used in the cookie name
	 *	cookieDuration	- (int) The length the cookie will last (in days)
	 *	errorMessage	- (string) Error message when AJAX calls fail
	 *	loadingMessage	- (string) Loading message while waiting for AJAX calls
	 *	tabsElement		- (string) The CSS query to grab the tab elements
	 *	sectionsElement	- (string) The CSS query to grab the section elements
	 *	template		- (string) Do not use an HTML template
	 *	onShow			- (function) Callback to trigger when a section is shown
	 */
	options: {
		ajax: true,
		fade: false,
		fadeDuration: 600,
		mode: 'click',
		activeClass: Titon.options.activeClass,
		defaultIndex: 0,
		persistState: false,
		cookie: null,
		cookieDuration: 30,
		errorMessage: Titon.msg.error,
		loadingMessage: Titon.msg.loading,
		tabsElement: 'nav a',
		sectionsElement: 'section',
		template: false,

		// Events
		onShow: null
	},

	/**
	 * Initialize Tabs by storing the query, gathering the elements and binding events.
	 *
	 * @param {String} query
	 * @param {Object} options
	 */
	initialize: function(query, options) {
		options.cookie = (options.cookie || query).camelCase();
		options.templateFrom = query;

		this.parent(options);
		this.query = query;

		// Get elements
		this.tabs = this.element.getElements(this.options.tabsElement);
		this.tabs.each(function(tab, index) {
			tab.set('data-tabs-index', index).removeClass(this.options.activeClass);
		}.bind(this));

		this.sections = this.element.getElements(this.options.sectionsElement);
		this.sections.hide();

		// Set events
		this.disable().enable();

		// Trigger default tab to display
		var index = Number.from(Cookie.read('titon.tabs.' + this.options.cookie) || this.options.defaultIndex);

		this.show(index);
	},

	/**
	 * Disable tab events.
	 *
	 * @return {Titon.Tabs}
	 */
	disable: function() {
		this.tabs.removeEvent((this.options.mode === 'click') ? 'click' : 'mouseover', this._listen);

		return this;
	},

	/**
	 * Enable tab events.
	 *
	 * @return {Titon.Tabs}
	 */
	enable: function() {
		this.tabs.addEvent((this.options.mode === 'click') ? 'click' : 'mouseover', this._listen);

		return this;
	},

	/**
	 * Show the content based on the tab. Can either pass an integer as the index in the collection,
	 * or pass an element object for a tab in the collection.
	 *
	 * @param {Element|int} tab
	 */
	show: function(tab) {
		if (typeOf(tab) === 'number') {
			tab = this.tabs[tab] || null;
		}

		if (!tab) {
			return;
		}

		var className = this.options.activeClass,
			target = tab.get('data-tabs-index'),
			url = tab.get('href'),
			section = this.sections[target];

		// Load content with AJAX
		if (url && !url.contains('#') && this.options.ajax) {
			if (!this.cache[url]) {
				new Request({
					url: url,
					method: 'get',
					evalScripts: true,
					onSuccess: function(response) {
						this.cache[url] = response;
						section.setHtml(response);
					}.bind(this),
					onRequest: function() {
						if (this.options.showLoading) {
							section.setHtml(new Element('div.tabs-loading', {
								text: this.options.loadingMessage
							}));
						}
					}.bind(this),
					onFailure: function() {
						section.setHtml(new Element('div.tabs-error', {
							text: this.options.errorMessage
						}));
					}.bind(this)
				}).get();
			}
		}

		// Toggle tabs
		this.tabs.removeClass(className);

		tab.addClass(className);

		// Toggle sections
		this.sections.hide();

		if (this.options.fade) {
			section.fadeIn(this.options.fadeDuration)
		} else {
			section.show();
		}

		// Persist the state using a cookie
		if (this.options.persistState) {
			Cookie.write('titon.tabs.' + this.options.cookie, tab.get('data-tabs-index'), {
				duration: this.options.cookieDuration
			});
		}

		// Track
		this.previousIndex = this.currentIndex;
		this.currentIndex = tab.get('data-tabs-index');

		this.fireEvent('show', tab);
	},

	/**
	 * Event callback for tab element click.
	 *
	 * @param {Event} e
	 */
	_listen: function(e) {
		e.stop();

		this.show(e.target);
	}

});

/**
 * All instances loaded via factory().
 */
Titon.Tabs.instances = {};

/**
 * Easily create multiple instances.
 *
 * @param {String} query
 * @param {Object} options
 * @return {Titon.Tabs}
 */
Titon.Tabs.factory = function(query, options) {
	if (Titon.Tabs.instances[query]) {
		return Titon.Tabs.instances[query];
	}

	var instance = new Titon.Tabs(query, options);

	Titon.Tabs.instances[query] = instance;

	return instance;
};<|endoftext|>


<commit_before>// An object representing a 2D vector.
// Based on the Vector2 class from LibGDX.
// Written by Rahat Ahmed (http://rahatah.me/d).

function Vec2(x, y) {
    this.x = x;
    this.y = y;
}

Vec2.prototype.add = function(d, m) {
    this.x += d.x * m;
    this.y += d.y * m;
    return this;
}

Vec2.prototype.sub = function(x, y) {
    if (x instanceof Vec2) {
        this.x -= x.x;
        this.y -= x.y;
    } else {
        this.x -= x;
        this.y -= y;
    }
    return this;
};

Vec2.prototype.sub2 = function(d, m) {
    this.x -= d.x * m;
    this.y -= d.y * m;
    return this;
};

Vec2.prototype.angle = function() {
    return Math.atan2(this.x, this.y);
};

Vec2.prototype.clone = function() {
    return new Vec2(this.x, this.y);
};

Vec2.prototype.dist = function() {
    return ~~this.x * ~~this.x + ~~this.y * ~~this.y;
};

Vec2.prototype.sqDist = function() {
    return Math.sqrt(this.dist());
};

Vec2.prototype.normalize = function() {
    return this.scale(1/this.sqDist());
};

Vec2.prototype.scale = function(scaleX, scaleY) {
    this.x *= scaleX;
    this.y *= scaleY || scaleX;
    return this;
};

module.exports = Vec2;
<commit_msg>Conversion to largest possible int sometimes returns 0<commit_after>// An object representing a 2D vector.
// Based on the Vector2 class from LibGDX.
// Written by Rahat Ahmed (http://rahatah.me/d).

function Vec2(x, y) {
    this.x = x;
    this.y = y;
}

Vec2.prototype.add = function(d, m) {
    this.x += d.x * m;
    this.y += d.y * m;
    return this;
}

Vec2.prototype.sub = function(x, y) {
    if (x instanceof Vec2) {
        this.x -= x.x;
        this.y -= x.y;
    } else {
        this.x -= x;
        this.y -= y;
    }
    return this;
};

Vec2.prototype.sub2 = function(d, m) {
    this.x -= d.x * m;
    this.y -= d.y * m;
    return this;
};

Vec2.prototype.angle = function() {
    return Math.atan2(this.x, this.y);
};

Vec2.prototype.clone = function() {
    return new Vec2(this.x, this.y);
};

Vec2.prototype.dist = function() {
    return this.x * this.x + this.y * this.y;
};

Vec2.prototype.sqDist = function() {
    return Math.sqrt(this.dist());
};

Vec2.prototype.normalize = function() {
    return this.scale(1/this.sqDist());
};

Vec2.prototype.scale = function(scaleX, scaleY) {
    this.x *= scaleX;
    this.y *= scaleY || scaleX;
    return this;
};

module.exports = Vec2;
<|endoftext|>


<commit_before>/* eslint-env node */
'use strict';

const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const isProduction = EmberApp.env() === 'production';

const purgeCSS = {
  module: require('@fullhuman/postcss-purgecss'),
  options: {
    content: [
      // add extra paths here for components/controllers which include tailwind classes
      './app/index.html',
      './app/components/**/*.hbs',
      './app/templates/**/*.hbs'
    ],
    safelistPatterns: [/pswp/],
    defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
  }
};

module.exports = function (defaults) {
  let app = new EmberApp(defaults, {
    postcssOptions: {
      compile: {
        plugins: [
          {
            module: require('postcss-import'),
            options: {
              path: ['node_modules']
            }
          },
          require('tailwindcss')('./app/tailwind/config.js'),
          ...(isProduction ? [purgeCSS] : [])
        ]
      }
    }
  });

  return app.toTree();
};
<commit_msg>properly specify purgeCSS safelist<commit_after>/* eslint-env node */
'use strict';

const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const isProduction = EmberApp.env() === 'production';

const purgeCSS = {
  module: require('@fullhuman/postcss-purgecss'),
  options: {
    content: [
      // add extra paths here for components/controllers which include tailwind classes
      './app/index.html',
      './app/components/**/*.hbs',
      './app/templates/**/*.hbs'
    ],
    safelist: [/pswp/],
    defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
  }
};

module.exports = function (defaults) {
  let app = new EmberApp(defaults, {
    postcssOptions: {
      compile: {
        plugins: [
          {
            module: require('postcss-import'),
            options: {
              path: ['node_modules']
            }
          },
          require('tailwindcss')('./app/tailwind/config.js'),
          ...(isProduction ? [purgeCSS] : [])
        ]
      }
    }
  });

  return app.toTree();
};
<|endoftext|>


<commit_before>import runloop from 'global/runloop';
import defineProperty from 'utils/defineProperty';
import getSpliceEquivalent from 'shared/getSpliceEquivalent';
import summariseSpliceOperation from 'shared/summariseSpliceOperation';
import processWrapper from 'viewmodel/prototype/get/arrayAdaptor/processWrapper';

var patchedArrayProto = [],
	mutatorMethods = [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ],
	testObj,
	patchArrayMethods,
	unpatchArrayMethods;

mutatorMethods.forEach( function ( methodName ) {
	var method = function () {
		var spliceEquivalent,
			newIndices,
			result,
			wrapper,
			i;

		// push, pop, shift and unshift can all be represented as a splice operation.
		// this makes life easier later
		spliceEquivalent = getSpliceEquivalent( this, methodName, Array.prototype.slice.call( arguments ) );

		if ( spliceEquivalent ) {
			newIndices = summariseSpliceOperation( this.length, spliceEquivalent );
		}

		// apply the underlying method
		result = Array.prototype[ methodName ].apply( this, arguments );

		// trigger changes
		this._ractive.setting = true;
		i = this._ractive.wrappers.length;
		while ( i-- ) {
			wrapper = this._ractive.wrappers[i];

			runloop.start( wrapper.root );
			processWrapper( wrapper, this, methodName, newIndices );
			runloop.end();
		}

		this._ractive.setting = false;
		return result;
	};

	defineProperty( patchedArrayProto, methodName, {
		value: method
	});
});

// can we use prototype chain injection?
// http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/#wrappers_prototype_chain_injection
testObj = {};

if ( testObj.__proto__ ) {
	// yes, we can
	patchArrayMethods = function ( array ) {
		array.__proto__ = patchedArrayProto;
	};

	unpatchArrayMethods = function ( array ) {
		array.__proto__ = Array.prototype;
	};
}

else {
	// no, we can't
	patchArrayMethods = function ( array ) {
		var i, methodName;

		i = mutatorMethods.length;
		while ( i-- ) {
			methodName = mutatorMethods[i];
			defineProperty( array, methodName, {
				value: patchedArrayProto[ methodName ],
				configurable: true
			});
		}
	};

	unpatchArrayMethods = function ( array ) {
		var i;

		i = mutatorMethods.length;
		while ( i-- ) {
			delete array[ mutatorMethods[i] ];
		}
	};
}

patchArrayMethods.unpatch = unpatchArrayMethods;
export default patchArrayMethods;
<commit_msg>array mutations should only trigger one turn of runloop<commit_after>import runloop from 'global/runloop';
import defineProperty from 'utils/defineProperty';
import getSpliceEquivalent from 'shared/getSpliceEquivalent';
import summariseSpliceOperation from 'shared/summariseSpliceOperation';
import processWrapper from 'viewmodel/prototype/get/arrayAdaptor/processWrapper';

var patchedArrayProto = [],
	mutatorMethods = [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ],
	testObj,
	patchArrayMethods,
	unpatchArrayMethods;

mutatorMethods.forEach( function ( methodName ) {
	var method = function () {
		var spliceEquivalent,
			newIndices,
			result,
			wrapper,
			i;

		// push, pop, shift and unshift can all be represented as a splice operation.
		// this makes life easier later
		spliceEquivalent = getSpliceEquivalent( this, methodName, Array.prototype.slice.call( arguments ) );

		if ( spliceEquivalent ) {
			newIndices = summariseSpliceOperation( this.length, spliceEquivalent );
		}

		// apply the underlying method
		result = Array.prototype[ methodName ].apply( this, arguments );

		// trigger changes
		runloop.start();

		this._ractive.setting = true;
		i = this._ractive.wrappers.length;
		while ( i-- ) {
			wrapper = this._ractive.wrappers[i];

			runloop.addViewmodel( wrapper.root.viewmodel );
			processWrapper( wrapper, this, methodName, newIndices );
		}

		runloop.end();

		this._ractive.setting = false;
		return result;
	};

	defineProperty( patchedArrayProto, methodName, {
		value: method
	});
});

// can we use prototype chain injection?
// http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/#wrappers_prototype_chain_injection
testObj = {};

if ( testObj.__proto__ ) {
	// yes, we can
	patchArrayMethods = function ( array ) {
		array.__proto__ = patchedArrayProto;
	};

	unpatchArrayMethods = function ( array ) {
		array.__proto__ = Array.prototype;
	};
}

else {
	// no, we can't
	patchArrayMethods = function ( array ) {
		var i, methodName;

		i = mutatorMethods.length;
		while ( i-- ) {
			methodName = mutatorMethods[i];
			defineProperty( array, methodName, {
				value: patchedArrayProto[ methodName ],
				configurable: true
			});
		}
	};

	unpatchArrayMethods = function ( array ) {
		var i;

		i = mutatorMethods.length;
		while ( i-- ) {
			delete array[ mutatorMethods[i] ];
		}
	};
}

patchArrayMethods.unpatch = unpatchArrayMethods;
export default patchArrayMethods;
<|endoftext|>


<commit_before>// Licence: PUBLIC DOMAIN <http://unlicense.org/>
// Author: Austin Wright <http://github.com/Acubed>

function MediaType(s, p){
	this.type = '';
	this.params = {};
	if(typeof s=='string'){
		var c = splitQuotedString(s);
		this.type = c.shift();
		for(var i=0; i<c.length; i++){
			this.parseParameter(c[i]);
		}
	}else if(s instanceof MediaType){
		this.type = s.type;
		this.q = s.q;
		for(var n in s.params) this.params[n]=s.params[n];
	}
	if(typeof p=='string'){
		var c = splitQuotedString(p);
		for(var i=0; i<c.length; i++){
			this.parseParameter(c[i]);
		}
	}else if(typeof p=='object'){
		for(var n in p) this.params[n]=p[n];
	}
}
MediaType.prototype.parseParameter = function parseParameter(s){
	var param = s.split('=',1);
	var name = param[0].trim();
	var value = s.substr(param[0].length+1).trim();
	if(!value || !name) return;
	if(name=='q' && this.q===undefined){
		this.q=parseFloat(value);
	}else{
		if(value[0]=='"' && value[value.length-1]=='"'){
			value = value.substr(1, value.length-2);
			value = value.replace(/\\(.)/g, function(a,b){return b;});
		}
		this.params[name]=value;
	}
}
MediaType.prototype.toString = function toString(){
	var str = this.type + ';q='+this.q;
	for(var n in this.params){
		str += ';'+n+'=';
		if(this.params[n].match(/["=;<>\[\]\(\) ,\-]/)){
			str += '"' + this.params[n].replace(/["\\]/g, function(a){return '\\'+a;}) + '"';
		}else{
			str += this.params[n];
		}
	}
	return str;
}
exports.MediaType = MediaType;

// Split a string by character, but ignore quoted parts and backslash-escaped characters
function splitQuotedString(str, delim, quote){
	delim = delim || ';';
	quote = quote || '"';
	var res = [];
	var start = 0;
	var offset = 0;
	function findNextChar(v, c, i, a){
		var p = str.indexOf(c, offset+1);
		return (p<0)?v:Math.min(p,v);
	}
	while(offset>=0){
		offset = [delim,quote].reduce(findNextChar, 1/0);
		if(offset===1/0) break;
		switch(str[offset]){
			case quote:
				// Skip to end of quoted string
				while(1){
					offset=str.indexOf(quote, offset+1);
					if(offset<0) break;
					if(str[offset-1]==='\\') continue;
					break;
				}
				continue;
			case delim:
				res.push(str.substr(start, offset-start).trim());
				start = ++offset;
				break;
		}
	}
	res.push(str.substr(start).trim());
	return res;
}
exports.splitQuotedString = splitQuotedString;

// Split a list of content types found in an Accept header
// Maybe use it like: splitContentTypes(request.headers.accept).map(parseMedia)
function splitContentTypes(str){
	return splitQuotedString(str, ',');
}
exports.splitContentTypes = splitContentTypes;

function parseMedia(str){
	var o = new MediaType(str);
	if(o.q===undefined) o.q=1;
	return o;
}
exports.parseMedia = parseMedia;

// Pick an ideal representation to send given a list of representations to choose from and the client-preferred list
function select(reps, accept){
	var cr = {q:0};
	var ca = {q:0};
	var cq = 0;
	for(var i=0; i<reps.length; i++){
		var r = reps[i];
		var rq = r.q || 1;
		for(var j=0; j<accept.length; j++){
			var a=accept[j];
			var aq = a.q || 1;
			var cmp = mediaCmp(a, r);
			if(cmp!==null && cmp>=0){
				if(aq*rq>cq){
					ca = a;
					cr = r;
					cq = ca.q*cr.q;
					if(cq===1 && cr.type) return cr;
				}
			}
		}
	}
	return cr.type&&cr;
}
exports.select = select;

// Determine if one media type is a subset of another
// If a is a superset of b (b is smaller than a), return 1
// If b is a superset of a, return -1
// If they are the exact same, return 0
// If they are disjoint, return null
function mediaCmp(a, b){
	if(a.type==='*/*' && b.type!=='*/*') return 1;
	else if(a.type!=='*/*' && b.type==='*/*') return -1;
	var ac = (a.type||'').split('/');
	var bc = (b.type||'').split('/');
	if(ac[0]=='*' && bc[0]!='*') return 1;
	if(ac[0]!='*' && bc[0]=='*') return -1;
	if(a.type!==b.type) return null;
	var ap = a.params || {};
	var bp = b.params || {};
	var ak = Object.keys(ap);
	var bk = Object.keys(bp);
	if(ak.length < bk.length) return 1;
	if(ak.length > bk.length) return -1;
	var k = ak.concat(bk).sort();
	var dir = 0;
	for(var n in ap){
		if(ap[n] && !bp[n]){ if(dir<0) return null; else dir=1; }
		if(!ap[n] && bp[n]){ if(dir>0) return null; else dir=-1; }
	}
	return dir;
}
exports.mediaCmp = mediaCmp;

return exports;
<commit_msg>Don't include an undefined q-value and order parameters constistently<commit_after>// Licence: PUBLIC DOMAIN <http://unlicense.org/>
// Author: Austin Wright <http://github.com/Acubed>

function MediaType(s, p){
	this.type = '';
	this.params = {};
	if(typeof s=='string'){
		var c = splitQuotedString(s);
		this.type = c.shift();
		for(var i=0; i<c.length; i++){
			this.parseParameter(c[i]);
		}
	}else if(s instanceof MediaType){
		this.type = s.type;
		this.q = s.q;
		for(var n in s.params) this.params[n]=s.params[n];
	}
	if(typeof p=='string'){
		var c = splitQuotedString(p);
		for(var i=0; i<c.length; i++){
			this.parseParameter(c[i]);
		}
	}else if(typeof p=='object'){
		for(var n in p) this.params[n]=p[n];
	}
}
MediaType.prototype.parseParameter = function parseParameter(s){
	var param = s.split('=',1);
	var name = param[0].trim();
	var value = s.substr(param[0].length+1).trim();
	if(!value || !name) return;
	if(name=='q' && this.q===undefined){
		this.q=parseFloat(value);
	}else{
		if(value[0]=='"' && value[value.length-1]=='"'){
			value = value.substr(1, value.length-2);
			value = value.replace(/\\(.)/g, function(a,b){return b;});
		}
		this.params[name]=value;
	}
}
MediaType.prototype.toString = function toString(){
	var str = this.type;
	if(this.q!==undefined) str += ';q='+this.q;
	var params = Object.keys(this.params).sort();
	for(var i=0; i<params.length; i++){
		var n = params[i];
		str += ';'+n+'=';
		if(this.params[n].match(/["=;<>\[\]\(\) ,\-]/)){
			str += '"' + this.params[n].replace(/["\\]/g, function(a){return '\\'+a;}) + '"';
		}else{
			str += this.params[n];
		}
	}
	return str;
}
exports.MediaType = MediaType;

// Split a string by character, but ignore quoted parts and backslash-escaped characters
function splitQuotedString(str, delim, quote){
	delim = delim || ';';
	quote = quote || '"';
	var res = [];
	var start = 0;
	var offset = 0;
	function findNextChar(v, c, i, a){
		var p = str.indexOf(c, offset+1);
		return (p<0)?v:Math.min(p,v);
	}
	while(offset>=0){
		offset = [delim,quote].reduce(findNextChar, 1/0);
		if(offset===1/0) break;
		switch(str[offset]){
			case quote:
				// Skip to end of quoted string
				while(1){
					offset=str.indexOf(quote, offset+1);
					if(offset<0) break;
					if(str[offset-1]==='\\') continue;
					break;
				}
				continue;
			case delim:
				res.push(str.substr(start, offset-start).trim());
				start = ++offset;
				break;
		}
	}
	res.push(str.substr(start).trim());
	return res;
}
exports.splitQuotedString = splitQuotedString;

// Split a list of content types found in an Accept header
// Maybe use it like: splitContentTypes(request.headers.accept).map(parseMedia)
function splitContentTypes(str){
	return splitQuotedString(str, ',');
}
exports.splitContentTypes = splitContentTypes;

function parseMedia(str){
	var o = new MediaType(str);
	if(o.q===undefined) o.q=1;
	return o;
}
exports.parseMedia = parseMedia;

// Pick an ideal representation to send given a list of representations to choose from and the client-preferred list
function select(reps, accept){
	var cr = {q:0};
	var ca = {q:0};
	var cq = 0;
	for(var i=0; i<reps.length; i++){
		var r = reps[i];
		var rq = r.q || 1;
		for(var j=0; j<accept.length; j++){
			var a=accept[j];
			var aq = a.q || 1;
			var cmp = mediaCmp(a, r);
			if(cmp!==null && cmp>=0){
				if(aq*rq>cq){
					ca = a;
					cr = r;
					cq = ca.q*cr.q;
					if(cq===1 && cr.type) return cr;
				}
			}
		}
	}
	return cr.type&&cr;
}
exports.select = select;

// Determine if one media type is a subset of another
// If a is a superset of b (b is smaller than a), return 1
// If b is a superset of a, return -1
// If they are the exact same, return 0
// If they are disjoint, return null
function mediaCmp(a, b){
	if(a.type==='*/*' && b.type!=='*/*') return 1;
	else if(a.type!=='*/*' && b.type==='*/*') return -1;
	var ac = (a.type||'').split('/');
	var bc = (b.type||'').split('/');
	if(ac[0]=='*' && bc[0]!='*') return 1;
	if(ac[0]!='*' && bc[0]=='*') return -1;
	if(a.type!==b.type) return null;
	var ap = a.params || {};
	var bp = b.params || {};
	var ak = Object.keys(ap);
	var bk = Object.keys(bp);
	if(ak.length < bk.length) return 1;
	if(ak.length > bk.length) return -1;
	var k = ak.concat(bk).sort();
	var dir = 0;
	for(var n in ap){
		if(ap[n] && !bp[n]){ if(dir<0) return null; else dir=1; }
		if(!ap[n] && bp[n]){ if(dir>0) return null; else dir=-1; }
	}
	return dir;
}
exports.mediaCmp = mediaCmp;

return exports;
<|endoftext|>


<commit_before>(function($) {

angular.module('ui.bootstrap.contextMenu', [])

.service('CustomService', function () {
    "use strict";

    return {
        initialize: function (item) {
            console.log("got here", item);
        }
    };

})
.directive('contextMenu', ["$parse", "$q", "CustomService", "$sce", function ($parse, $q, custom, $sce) {

    var _contextMenus = [];
    // Contains the element that was clicked to show the context menu
    var _clickedElement = null;
    var DEFAULT_ITEM_TEXT = "New Item";

    function createAndAddOptionText(params) {
        // Destructuring:
        var $scope = params.$scope;
        var item = params.item;
        var event = params.event;
        var modelValue = params.modelValue;
        var $promises = params.$promises;
        var nestedMenu = params.nestedMenu;
        var $li = params.$li;
        var leftOriented = String(params.orientation).toLowerCase() === 'left';

        var optionText = null;

        if (item.html) {
          if (angular.isFunction(item.html)) {
            // runs the function that expects a jQuery/jqLite element
            optionText = item.html($scope);
          } else {
            // Assumes that the developer already placed a valid jQuery/jqLite element
            optionText = item.html;
          }
        } else {

          var $a = $('');
          var $anchorStyle = {};

          if (leftOriented) {
              $anchorStyle.textAlign = 'right';
              $anchorStyle.paddingLeft = "8px";
            } else {
              $anchorStyle.textAlign = 'left';
              $anchorStyle.paddingRight = "8px";
            }

          $a.css($anchorStyle);
          $a.attr({ tabindex: '-1', href: '#' });

          var textParam = item[0];
          var text = DEFAULT_ITEM_TEXT;

          if (typeof textParam === 'string') {
              text = textParam;
          } else if (typeof textParam === "function") {
              text = textParam.call($scope, $scope, event, modelValue);
          } else if (typeof item.text !== "undefined") {
              text = item.text;
          }

          var $promise = $q.when(text);
          $promises.push($promise);
          $promise.then(function (pText) {
              if (nestedMenu) {
                  var $arrow;
                  var $boldStyle = {
                      fontFamily: 'monospace',
                      fontWeight: 'bold'
                  };

                  if (leftOriented) {
                      $arrow = '<';
                      $boldStyle.float = 'left';
                  } else {
                      $arrow = '>';
                      $boldStyle.float = 'right';
                  }

                  var $bold = $('' + $arrow + '');
                  $bold.css($boldStyle);
                  $a.css("cursor", "default");
                  $a.append($bold);
              }
              $a.append(pText);
          });

          optionText = $a;
        }

        $li.append(optionText);
    };

    /**
     * Process each individual item
     *
     * Properties of params:
     * - $scope
     * - event
     * - modelValue
     * - level
     * - item
     * - $ul
     * - $li
     * - $promises
     */
    function processItem(params) {
        var nestedMenu = extractNestedMenu(params);

        // if html property is not defined, fallback to text, otherwise use default text
        // if first item in the item array is a function then invoke .call()
        // if first item is a string, then text should be the string.

        var text = DEFAULT_ITEM_TEXT;
        var currItemParam = angular.extend({}, params);
        currItemParam.nestedMenu = nestedMenu;
        currItemParam.enabled = isOptionEnabled(currItemParam);
        currItemParam.text = createAndAddOptionText(currItemParam);

        registerCurrentItemEvents(currItemParam);

    };

    /*
     * Registers the appropriate mouse events for options if the item is enabled.
     * Otherwise, it ensures that clicks to the item do not propagate.
     */
    function registerCurrentItemEvents (params) {
        // Destructuring:
        var item = params.item;
        var $ul = params.$ul;
        var $li = params.$li;
        var $scope = params.$scope;
        var modelValue = params.modelValue;
        var level = params.level;
        var event = params.event;
        var text = params.text;
        var nestedMenu = params.nestedMenu;
        var enabled = params.enabled;
        var orientation = String(params.orientation).toLowerCase();

        if (enabled) {
            var openNestedMenu = function ($event) {
                removeContextMenus(level + 1);
                /*
                 * The object here needs to be constructed and filled with data
                 * on an "as needed" basis. Copying the data from event directly
                 * or cloning the event results in unpredictable behavior.
                 */
                /// adding the original event in the object to use the attributes of the mouse over event in the promises
                var ev = {
                    pageX: orientation === 'left' ? event.pageX - $ul[0].offsetWidth + 1 : event.pageX + $ul[0].offsetWidth - 1,
                    pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
                    view: event.view || window,
                    event: $event
                };

                /*
                 * At this point, nestedMenu can only either be an Array or a promise.
                 * Regardless, passing them to `when` makes the implementation singular.
                 */
                $q.when(nestedMenu).then(function(promisedNestedMenu) {
                    var nestedParam = {
                      "$scope" : $scope,
                      "event" : ev,
                      "options" : promisedNestedMenu,
                      "modelValue" : modelValue,
                      "level" : level + 1,
                      "orientation": orientation
                    };
                    renderContextMenu(nestedParam);
                });
            };

            $li.on('click', function ($event) {
                if($event.which == 1) {
                  $event.preventDefault();
                  $scope.$apply(function () {
                    $(event.currentTarget).removeClass('context');
                    removeAllContextMenus();

                    var clickFunction = angular.isFunction(item.click)
                      ? item.click
                      : (angular.isFunction(item[1])
                          ? item[1]
                          : null);

                    if (clickFunction) {
                      clickFunction.call($scope, $scope, event, modelValue, text, $li);
                    }
                  });
                }
            });

            $li.on('mouseover', function ($event) {
                $scope.$apply(function () {
                    if (nestedMenu) {
                        openNestedMenu($event);
                    } else {
                        removeContextMenus(level + 1);
                    }
                });
            });
        } else {
            $li.on('click', function ($event) {
                $event.preventDefault();
            });
            $li.addClass('disabled');
        }
    };

    /**
     * @param params - an object containing the `item` parameter
     * @returns an Array or a Promise containing the children,
     *          or null if the option has no submenu
     */
    function extractNestedMenu(params) {
      // Destructuring:
      var item = params.item;

      // New implementation:
      if (item.children) {
        if (angular.isFunction(item.children)) {
          // Expects a function that returns a Promise or an Array
          return item.children();
        } else if (angular.isFunction(item.children.then) || angular.isArray(item.children)) {
          // Returns the promise
          // OR, returns the actual array
          return item.children;
        }

        return null;

      } else {
        // nestedMenu is either an Array or a Promise that will return that array.
        // NOTE: This might be changed soon as it's a hangover from an old implementation

        return angular.isArray(item[1]) ||
            (item[1] && angular.isFunction(item[1].then)) ? item[1] : angular.isArray(item[2]) ||
            (item[2] && angular.isFunction(item[2].then)) ? item[2] : angular.isArray(item[3]) ||
            (item[3] && angular.isFunction(item[3].then)) ? item[3] : null;
      }
    }

    /**
     * Responsible for the actual rendering of the context menu.
     *
     * The parameters in params are:
     * - $scope = the scope of this context menu
     * - event = the event that triggered this context menu
     * - options = the options for this context menu
     * - modelValue = the value of the model attached to this context menu
     * - level = the current context menu level (defauts to 0)
     * - customClass = the custom class to be used for the context menu
     */
    function renderContextMenu (params) {
        /// Render context menu recursively.

        // Destructuring:
        var $scope = params.$scope;
        var event = params.event;
        var options = params.options;
        var modelValue = params.modelValue;
        var level = params.level;
        var customClass = params.customClass;

        // Initialize the container. This will be passed around
        var $ul = initContextMenuContainer(params);
        params.$ul = $ul;

        // Register this level of the context menu
        _contextMenus.push($ul);

        /*
         * This object will contain any promises that we have
         * to wait for before trying to adjust the context menu.
         */
        var $promises = [];
        params.$promises = $promises;

        angular.forEach(options, function (item) {
            var $li = $('
');
            if (item === null) {
                $li.addClass('divider');
            } else if (typeof item[0] === "object") {
                custom.initialize($li, item);
            } else {
                var itemParams = angular.extend({}, params);
                itemParams.item = item;
                itemParams.$li = $li;
                processItem(itemParams);
            }
            $ul.append($li);
        });

        $(document).find('body').append($ul);

        doAfterAllPromises(params);
    };

    /**
     * calculate if drop down menu would go out of screen at left or bottom
     * calculation need to be done after element has been added (and all texts are set; thus the promises)
     * to the DOM the get the actual height
     */
    function doAfterAllPromises (params) {

        // Desctructuring:
        var $ul = params.$ul;
        var $promises = params.$promises;
        var level = params.level;
        var event = params.event;
        var leftOriented = String(params.orientation).toLowerCase() === 'left';

        $q.all($promises).then(function () {
            var topCoordinate  = event.pageY;
            var menuHeight = angular.element($ul[0]).prop('offsetHeight');
            var winHeight = window.scrollY + event.view.innerHeight;
            /// the 20 pixels in second condition are considering the browser status bar that sometimes overrides the element
            if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight + 20) {
                topCoordinate = event.pageY - menuHeight;
                /// If the element is a nested menu, adds the height of the parent li to the topCoordinate to align with the parent
                if(level && level > 0) {
                  topCoordinate += event.event.currentTarget.offsetHeight;
                }
            } else if(winHeight <= menuHeight) {
                // If it really can't fit, reset the height of the menu to one that will fit
                angular.element($ul[0]).css({"height": winHeight - 5, "overflow-y": "scroll"});
                // ...then set the topCoordinate height to 0 so the menu starts from the top
                topCoordinate = 0;
            } else if(winHeight - topCoordinate < menuHeight) {
                var reduceThresholdY = 5;
                if(topCoordinate < reduceThresholdY) {
                    reduceThresholdY = topCoordinate;
                }
                topCoordinate = winHeight - menuHeight - reduceThresholdY;
            }

            var leftCoordinate = event.pageX;
            var menuWidth = angular.element($ul[0]).prop('offsetWidth');
            var winWidth = event.view.innerWidth;
            var padding = 5;

            if (leftOriented) {
                if (winWidth - leftCoordinate > menuWidth && leftCoordinate < menuWidth + padding) {
                    leftCoordinate = padding;
                } else if (leftCoordinate < menuWidth) {
                    var reduceThresholdX = 5;
                    if (winWidth - leftCoordinate < reduceThresholdX + padding) {
                        reduceThresholdX = winWidth - leftCoordinate + padding;
                    }
                    leftCoordinate = menuWidth + reduceThresholdX + padding;
                } else {
                    leftCoordinate = leftCoordinate - menuWidth;
                }
            } else {
                if (leftCoordinate > menuWidth && winWidth - leftCoordinate - padding < menuWidth) {
                    leftCoordinate = winWidth - menuWidth - padding;
                } else if(winWidth - leftCoordinate < menuWidth) {
                    var reduceThresholdX = 5;
                    if(leftCoordinate < reduceThresholdX + padding) {
                        reduceThresholdX = leftCoordinate + padding;
                    }
                    leftCoordinate = winWidth - menuWidth - reduceThresholdX - padding;
                }
            }

            $ul.css({
                display: 'block',
                position: 'absolute',
                left: leftCoordinate + 'px',
                top: topCoordinate + 'px'
            });
        });

    };

    /**
     * Creates the container of the context menu (a
 element),
     * applies the appropriate styles and then returns that container
     *
     * @return a
 jqLite/jQuery element
     */
    function initContextMenuContainer(params) {
      // Destructuring
      var customClass = params.customClass;

      var $ul = $('
');
      $ul.addClass('dropdown-menu');
      $ul.attr({ 'role': 'menu' });
      $ul.css({
          display: 'block',
          position: 'absolute',
          left: params.event.pageX + 'px',
          top: params.event.pageY + 'px',
          "z-index": 10000
      });

      if(customClass) { $ul.addClass(customClass); }

      return $ul;
    }

    // if item is object, and has enabled prop invoke the prop
    // else if fallback to item[2]
    function isOptionEnabled (params) {
        var item = params.item;
        var $scope = params.$scope;
        var event = params.event;
        var modelValue = params.modelValue;
        var text = params.text;

        if (typeof item.enabled !== "undefined") {
            return item.enabled.call($scope, $scope, event, modelValue, text);
        } else if (typeof item[2] === "function") {
            return item[2].call($scope, $scope, event, modelValue, text);
        } else {
            return true;
        }
    };

    function isTouchDevice() {
      return 'ontouchstart' in window  || navigator.maxTouchPoints; // works on most browsers | works on IE10/11 and Surface
    }

    /**
     * Removes the context menus with level greater than or equal
     * to the value passed. If undefined, null or 0, all context menus
     * are removed.
     */
    function removeContextMenus (level) {
        while (_contextMenus.length && (!level || _contextMenus.length > level)) {
            _contextMenus.pop().remove();
        }
    }

    function removeOnScrollEvent(e) {
        removeAllContextMenus(e);
    }

    function removeOnOutsideClickEvent(e) {

      var $curr = $(e.target);
      var shouldRemove = true;

      while($curr.length) {
        if($curr.hasClass("dropdown-menu")) {
          shouldRemove = false;
          break;
        } else {
          $curr = $curr.parent();
        }
      }
      if (shouldRemove) {
        removeAllContextMenus(e);
      }
    }

    function removeAllContextMenus(e) {
        $(document.body).off('mousedown', removeOnOutsideClickEvent);
        $(document).off('scroll', removeOnScrollEvent);
        $(_clickedElement).removeClass('context');
        removeContextMenus();
    }

    return function ($scope, element, attrs) {
        var openMenuEvent = "contextmenu";
        if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === "string"){
            openMenuEvent = attrs.contextMenuOn;
        }
        element.on(openMenuEvent, function (event) {
            if(!attrs.allowEventPropagation) {
              event.stopPropagation();
              event.preventDefault();
            }

            // Don't show context menu if on touch device and element is draggable
            if(isTouchDevice() && element.attr('draggable') === 'true') {
              return false;
            }

            // Remove if the user clicks outside
            $(document.body).on('mousedown', removeOnOutsideClickEvent);
            // Remove the menu when the scroll moves
            $(document).on('scroll', removeOnScrollEvent);

            _clickedElement = event.currentTarget;
            $(_clickedElement).addClass('context');

            $scope.$apply(function () {
                var options = $scope.$eval(attrs.contextMenu);
                var customClass = attrs.contextMenuClass;
                var modelValue = $scope.$eval(attrs.model);
                var orientation = attrs.contextMenuOrientation;

                var params = {
                  "$scope" : $scope,
                  "event" : event,
                  "options" : options,
                  "modelValue" : modelValue,
                  "level" : 0,
                  "customClass" : customClass,
                  "orientation": orientation
                };

                if (options instanceof Array) {
                    if (options.length === 0) { return; }
                    renderContextMenu(params);
                } else {
                    throw '"' + attrs.contextMenu + '" not an array';
                }
            });

            // Remove all context menus if the scope is destroyed
            $scope.$on("$destroy", function () {
                removeAllContextMenus();
            });
        });
    };
}]);

})(window.angular.element);
<commit_msg>fix: add target to event passed to submenus<commit_after>(function($) {

angular.module('ui.bootstrap.contextMenu', [])

.service('CustomService', function () {
    "use strict";

    return {
        initialize: function (item) {
            console.log("got here", item);
        }
    };

})
.directive('contextMenu', ["$parse", "$q", "CustomService", "$sce", function ($parse, $q, custom, $sce) {

    var _contextMenus = [];
    // Contains the element that was clicked to show the context menu
    var _clickedElement = null;
    var DEFAULT_ITEM_TEXT = "New Item";

    function createAndAddOptionText(params) {
        // Destructuring:
        var $scope = params.$scope;
        var item = params.item;
        var event = params.event;
        var modelValue = params.modelValue;
        var $promises = params.$promises;
        var nestedMenu = params.nestedMenu;
        var $li = params.$li;
        var leftOriented = String(params.orientation).toLowerCase() === 'left';

        var optionText = null;

        if (item.html) {
          if (angular.isFunction(item.html)) {
            // runs the function that expects a jQuery/jqLite element
            optionText = item.html($scope);
          } else {
            // Assumes that the developer already placed a valid jQuery/jqLite element
            optionText = item.html;
          }
        } else {

          var $a = $('');
          var $anchorStyle = {};

          if (leftOriented) {
              $anchorStyle.textAlign = 'right';
              $anchorStyle.paddingLeft = "8px";
            } else {
              $anchorStyle.textAlign = 'left';
              $anchorStyle.paddingRight = "8px";
            }

          $a.css($anchorStyle);
          $a.attr({ tabindex: '-1', href: '#' });

          var textParam = item[0];
          var text = DEFAULT_ITEM_TEXT;

          if (typeof textParam === 'string') {
              text = textParam;
          } else if (typeof textParam === "function") {
              text = textParam.call($scope, $scope, event, modelValue);
          } else if (typeof item.text !== "undefined") {
              text = item.text;
          }

          var $promise = $q.when(text);
          $promises.push($promise);
          $promise.then(function (pText) {
              if (nestedMenu) {
                  var $arrow;
                  var $boldStyle = {
                      fontFamily: 'monospace',
                      fontWeight: 'bold'
                  };

                  if (leftOriented) {
                      $arrow = '<';
                      $boldStyle.float = 'left';
                  } else {
                      $arrow = '>';
                      $boldStyle.float = 'right';
                  }

                  var $bold = $('' + $arrow + '');
                  $bold.css($boldStyle);
                  $a.css("cursor", "default");
                  $a.append($bold);
              }
              $a.append(pText);
          });

          optionText = $a;
        }

        $li.append(optionText);
    };

    /**
     * Process each individual item
     *
     * Properties of params:
     * - $scope
     * - event
     * - modelValue
     * - level
     * - item
     * - $ul
     * - $li
     * - $promises
     */
    function processItem(params) {
        var nestedMenu = extractNestedMenu(params);

        // if html property is not defined, fallback to text, otherwise use default text
        // if first item in the item array is a function then invoke .call()
        // if first item is a string, then text should be the string.

        var text = DEFAULT_ITEM_TEXT;
        var currItemParam = angular.extend({}, params);
        currItemParam.nestedMenu = nestedMenu;
        currItemParam.enabled = isOptionEnabled(currItemParam);
        currItemParam.text = createAndAddOptionText(currItemParam);

        registerCurrentItemEvents(currItemParam);

    };

    /*
     * Registers the appropriate mouse events for options if the item is enabled.
     * Otherwise, it ensures that clicks to the item do not propagate.
     */
    function registerCurrentItemEvents (params) {
        // Destructuring:
        var item = params.item;
        var $ul = params.$ul;
        var $li = params.$li;
        var $scope = params.$scope;
        var modelValue = params.modelValue;
        var level = params.level;
        var event = params.event;
        var text = params.text;
        var nestedMenu = params.nestedMenu;
        var enabled = params.enabled;
        var orientation = String(params.orientation).toLowerCase();

        if (enabled) {
            var openNestedMenu = function ($event) {
                removeContextMenus(level + 1);
                /*
                 * The object here needs to be constructed and filled with data
                 * on an "as needed" basis. Copying the data from event directly
                 * or cloning the event results in unpredictable behavior.
                 */
                /// adding the original event in the object to use the attributes of the mouse over event in the promises
                var ev = {
                    pageX: orientation === 'left' ? event.pageX - $ul[0].offsetWidth + 1 : event.pageX + $ul[0].offsetWidth - 1,
                    pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
                    view: event.view || window,
                    target: event.target,
                    event: $event
                };

                /*
                 * At this point, nestedMenu can only either be an Array or a promise.
                 * Regardless, passing them to `when` makes the implementation singular.
                 */
                $q.when(nestedMenu).then(function(promisedNestedMenu) {
                    var nestedParam = {
                      "$scope" : $scope,
                      "event" : ev,
                      "options" : promisedNestedMenu,
                      "modelValue" : modelValue,
                      "level" : level + 1,
                      "orientation": orientation
                    };
                    renderContextMenu(nestedParam);
                });
            };

            $li.on('click', function ($event) {
                if($event.which == 1) {
                  $event.preventDefault();
                  $scope.$apply(function () {
                    $(event.currentTarget).removeClass('context');
                    removeAllContextMenus();

                    var clickFunction = angular.isFunction(item.click)
                      ? item.click
                      : (angular.isFunction(item[1])
                          ? item[1]
                          : null);

                    if (clickFunction) {
                      clickFunction.call($scope, $scope, event, modelValue, text, $li);
                    }
                  });
                }
            });

            $li.on('mouseover', function ($event) {
                $scope.$apply(function () {
                    if (nestedMenu) {
                        openNestedMenu($event);
                    } else {
                        removeContextMenus(level + 1);
                    }
                });
            });
        } else {
            $li.on('click', function ($event) {
                $event.preventDefault();
            });
            $li.addClass('disabled');
        }
    };

    /**
     * @param params - an object containing the `item` parameter
     * @returns an Array or a Promise containing the children,
     *          or null if the option has no submenu
     */
    function extractNestedMenu(params) {
      // Destructuring:
      var item = params.item;

      // New implementation:
      if (item.children) {
        if (angular.isFunction(item.children)) {
          // Expects a function that returns a Promise or an Array
          return item.children();
        } else if (angular.isFunction(item.children.then) || angular.isArray(item.children)) {
          // Returns the promise
          // OR, returns the actual array
          return item.children;
        }

        return null;

      } else {
        // nestedMenu is either an Array or a Promise that will return that array.
        // NOTE: This might be changed soon as it's a hangover from an old implementation

        return angular.isArray(item[1]) ||
            (item[1] && angular.isFunction(item[1].then)) ? item[1] : angular.isArray(item[2]) ||
            (item[2] && angular.isFunction(item[2].then)) ? item[2] : angular.isArray(item[3]) ||
            (item[3] && angular.isFunction(item[3].then)) ? item[3] : null;
      }
    }

    /**
     * Responsible for the actual rendering of the context menu.
     *
     * The parameters in params are:
     * - $scope = the scope of this context menu
     * - event = the event that triggered this context menu
     * - options = the options for this context menu
     * - modelValue = the value of the model attached to this context menu
     * - level = the current context menu level (defauts to 0)
     * - customClass = the custom class to be used for the context menu
     */
    function renderContextMenu (params) {
        /// Render context menu recursively.

        // Destructuring:
        var $scope = params.$scope;
        var event = params.event;
        var options = params.options;
        var modelValue = params.modelValue;
        var level = params.level;
        var customClass = params.customClass;

        // Initialize the container. This will be passed around
        var $ul = initContextMenuContainer(params);
        params.$ul = $ul;

        // Register this level of the context menu
        _contextMenus.push($ul);

        /*
         * This object will contain any promises that we have
         * to wait for before trying to adjust the context menu.
         */
        var $promises = [];
        params.$promises = $promises;

        angular.forEach(options, function (item) {
            var $li = $('
');
            if (item === null) {
                $li.addClass('divider');
            } else if (typeof item[0] === "object") {
                custom.initialize($li, item);
            } else {
                var itemParams = angular.extend({}, params);
                itemParams.item = item;
                itemParams.$li = $li;
                processItem(itemParams);
            }
            $ul.append($li);
        });

        $(document).find('body').append($ul);

        doAfterAllPromises(params);
    };

    /**
     * calculate if drop down menu would go out of screen at left or bottom
     * calculation need to be done after element has been added (and all texts are set; thus the promises)
     * to the DOM the get the actual height
     */
    function doAfterAllPromises (params) {

        // Desctructuring:
        var $ul = params.$ul;
        var $promises = params.$promises;
        var level = params.level;
        var event = params.event;
        var leftOriented = String(params.orientation).toLowerCase() === 'left';

        $q.all($promises).then(function () {
            var topCoordinate  = event.pageY;
            var menuHeight = angular.element($ul[0]).prop('offsetHeight');
            var winHeight = window.scrollY + event.view.innerHeight;
            /// the 20 pixels in second condition are considering the browser status bar that sometimes overrides the element
            if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight + 20) {
                topCoordinate = event.pageY - menuHeight;
                /// If the element is a nested menu, adds the height of the parent li to the topCoordinate to align with the parent
                if(level && level > 0) {
                  topCoordinate += event.event.currentTarget.offsetHeight;
                }
            } else if(winHeight <= menuHeight) {
                // If it really can't fit, reset the height of the menu to one that will fit
                angular.element($ul[0]).css({"height": winHeight - 5, "overflow-y": "scroll"});
                // ...then set the topCoordinate height to 0 so the menu starts from the top
                topCoordinate = 0;
            } else if(winHeight - topCoordinate < menuHeight) {
                var reduceThresholdY = 5;
                if(topCoordinate < reduceThresholdY) {
                    reduceThresholdY = topCoordinate;
                }
                topCoordinate = winHeight - menuHeight - reduceThresholdY;
            }

            var leftCoordinate = event.pageX;
            var menuWidth = angular.element($ul[0]).prop('offsetWidth');
            var winWidth = event.view.innerWidth;
            var padding = 5;

            if (leftOriented) {
                if (winWidth - leftCoordinate > menuWidth && leftCoordinate < menuWidth + padding) {
                    leftCoordinate = padding;
                } else if (leftCoordinate < menuWidth) {
                    var reduceThresholdX = 5;
                    if (winWidth - leftCoordinate < reduceThresholdX + padding) {
                        reduceThresholdX = winWidth - leftCoordinate + padding;
                    }
                    leftCoordinate = menuWidth + reduceThresholdX + padding;
                } else {
                    leftCoordinate = leftCoordinate - menuWidth;
                }
            } else {
                if (leftCoordinate > menuWidth && winWidth - leftCoordinate - padding < menuWidth) {
                    leftCoordinate = winWidth - menuWidth - padding;
                } else if(winWidth - leftCoordinate < menuWidth) {
                    var reduceThresholdX = 5;
                    if(leftCoordinate < reduceThresholdX + padding) {
                        reduceThresholdX = leftCoordinate + padding;
                    }
                    leftCoordinate = winWidth - menuWidth - reduceThresholdX - padding;
                }
            }

            $ul.css({
                display: 'block',
                position: 'absolute',
                left: leftCoordinate + 'px',
                top: topCoordinate + 'px'
            });
        });

    };

    /**
     * Creates the container of the context menu (a
 element),
     * applies the appropriate styles and then returns that container
     *
     * @return a
 jqLite/jQuery element
     */
    function initContextMenuContainer(params) {
      // Destructuring
      var customClass = params.customClass;

      var $ul = $('
');
      $ul.addClass('dropdown-menu');
      $ul.attr({ 'role': 'menu' });
      $ul.css({
          display: 'block',
          position: 'absolute',
          left: params.event.pageX + 'px',
          top: params.event.pageY + 'px',
          "z-index": 10000
      });

      if(customClass) { $ul.addClass(customClass); }

      return $ul;
    }

    // if item is object, and has enabled prop invoke the prop
    // else if fallback to item[2]
    function isOptionEnabled (params) {
        var item = params.item;
        var $scope = params.$scope;
        var event = params.event;
        var modelValue = params.modelValue;
        var text = params.text;

        if (typeof item.enabled !== "undefined") {
            return item.enabled.call($scope, $scope, event, modelValue, text);
        } else if (typeof item[2] === "function") {
            return item[2].call($scope, $scope, event, modelValue, text);
        } else {
            return true;
        }
    };

    function isTouchDevice() {
      return 'ontouchstart' in window  || navigator.maxTouchPoints; // works on most browsers | works on IE10/11 and Surface
    }

    /**
     * Removes the context menus with level greater than or equal
     * to the value passed. If undefined, null or 0, all context menus
     * are removed.
     */
    function removeContextMenus (level) {
        while (_contextMenus.length && (!level || _contextMenus.length > level)) {
            _contextMenus.pop().remove();
        }
    }

    function removeOnScrollEvent(e) {
        removeAllContextMenus(e);
    }

    function removeOnOutsideClickEvent(e) {

      var $curr = $(e.target);
      var shouldRemove = true;

      while($curr.length) {
        if($curr.hasClass("dropdown-menu")) {
          shouldRemove = false;
          break;
        } else {
          $curr = $curr.parent();
        }
      }
      if (shouldRemove) {
        removeAllContextMenus(e);
      }
    }

    function removeAllContextMenus(e) {
        $(document.body).off('mousedown', removeOnOutsideClickEvent);
        $(document).off('scroll', removeOnScrollEvent);
        $(_clickedElement).removeClass('context');
        removeContextMenus();
    }

    return function ($scope, element, attrs) {
        var openMenuEvent = "contextmenu";
        if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === "string"){
            openMenuEvent = attrs.contextMenuOn;
        }
        element.on(openMenuEvent, function (event) {
            if(!attrs.allowEventPropagation) {
              event.stopPropagation();
              event.preventDefault();
            }

            // Don't show context menu if on touch device and element is draggable
            if(isTouchDevice() && element.attr('draggable') === 'true') {
              return false;
            }

            // Remove if the user clicks outside
            $(document.body).on('mousedown', removeOnOutsideClickEvent);
            // Remove the menu when the scroll moves
            $(document).on('scroll', removeOnScrollEvent);

            _clickedElement = event.currentTarget;
            $(_clickedElement).addClass('context');

            $scope.$apply(function () {
                var options = $scope.$eval(attrs.contextMenu);
                var customClass = attrs.contextMenuClass;
                var modelValue = $scope.$eval(attrs.model);
                var orientation = attrs.contextMenuOrientation;

                var params = {
                  "$scope" : $scope,
                  "event" : event,
                  "options" : options,
                  "modelValue" : modelValue,
                  "level" : 0,
                  "customClass" : customClass,
                  "orientation": orientation
                };

                if (options instanceof Array) {
                    if (options.length === 0) { return; }
                    renderContextMenu(params);
                } else {
                    throw '"' + attrs.contextMenu + '" not an array';
                }
            });

            // Remove all context menus if the scope is destroyed
            $scope.$on("$destroy", function () {
                removeAllContextMenus();
            });
        });
    };
}]);

})(window.angular.element);
<|endoftext|>