.rc-modal {
	display: flex;
	flex-direction: column;

	min-width: 400px;
	max-width: 500px;
	height: auto;
	max-height: 90%;

	padding: 1rem;

	animation: dropdown-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);

	border: none;

	background: white;

	box-shadow: 0 0 2px 0 rgba(47, 52, 61, 0.08), 0 0 12px 0 rgba(47, 52, 61, 0.12);

	&-wrapper {
		position: fixed;
		z-index: 10;
		top: 0;
		right: 0;
		bottom: 0;
		left: 0;

		display: flex;

		padding: 10px;

		background: rgba(47, 52, 61, 0.8);
		align-items: center;
		justify-content: center;
	}

	&--modal {
		width: 640px;
		max-width: 100%;
	}

	&--modal &__title {
		font-size: 1.375rem;
		font-weight: 500;
	}

	&__description {
		font-size: 0.874rem;
		margin-block-end: 1.5rem;
		margin-block-start: 0.5rem;
	}

	&__title {
		flex: 1 1 auto;

		font-size: 1rem;
	}

	&__close {
		cursor: pointer;
		transform: rotate(45deg);

		font-size: 20px;
	}

	&__header {
		display: flex;
		flex-direction: row;
		flex: 0 0 auto;

		margin-bottom: -16px;
		padding: 16px;

		font-size: 21px;
		justify-content: flex-end;
	}

	&__content {
		position: relative;

		display: flex;

		overflow: hidden auto;

		flex-direction: column;

		min-height: 72px;
		max-height: 90%;

		padding: 16px;

		animation: dropdown-show 0.1s cubic-bezier(0.45, 0.05, 0.55, 0.95);
		align-items: stretch;
	}

	&__content-icon {
		margin: 1.2rem auto;

		font-size: 6rem;

		&--modal-warning {
			color: var(--rc-color-alert);
		}

		&--modal-success {
			color: var(--rc-color-success-light);
		}

		&--modal-info {
			color: var(--rc-color-alert);
		}

		&--modal-error {
			color: var(--rc-color-error-light);
		}
	}

	&__content-text {
		text-align: center;
		word-break: break-word;

		font-size: 1rem;
		line-height: 1.5;
	}

	&__content-error {
		display: none;

		width: 100%;
		margin: 0.5rem 0;

		text-align: center;
		word-break: break-word;

		color: var(--color-white);
		background: var(--rc-color-error);

		line-height: 2;
	}

	&__footer {
		display: flex;

		flex: 0 0 auto;

		padding: 1rem;

		justify-content: space-between;

		& > .rc-button {
			margin: 0;
		}
	}
}

@media (width <= 400px) {
	.rc-modal {
		top: 0 !important;
		bottom: 0;
		left: 0 !important;

		width: 100%;
		min-width: 100%;
		max-width: 100%;

		animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
	}
}


ScribeDOM =
  ELEMENT_NODE: 1
  NOBREAK_SPACE:  " "
  TEXT_NODE: 3
  ZERO_WIDTH_NOBREAK_SPACE:  "\uFEFF"

  addClass: (node, cssClass) ->
    return if ScribeDOM.hasClass(node, cssClass)
    if node.classList?
      node.classList.add(cssClass)
    else if node.className?
      node.className += ' ' + cssClass

  addEventListener: (node, eventName, listener) ->
    names = eventName.split(' ')
    if names.length > 1
      return _.each(names, (name) ->
        ScribeDOM.addEventListener(node, name, listener)
      )
    callback = (event) ->
      event ?= ScribeDOM.getWindow(node).event
      event.target ?= event.srcElement
      event.which ?= event.keyCode
      bubbles = listener.call(null, event)
      if bubbles == false
        if event.preventDefault? and event.stopPropagation?
          event.preventDefault()
          event.stopPropagation()
        else
          event.returnValue = false
      return bubbles
    if node.addEventListener?
      node.addEventListener(eventName, callback)
    else if node.attachEvent?
      node.attachEvent("on#{eventName}", callback)
    else
      throw new Error("No add event support")

  getChildIndex: (node) ->
    index = 0
    while node.previousSibling?
      node = previousSibling
      index += 1
    return index

  getClasses: (node) ->
    if node.classList
      return _.clone(node.classList)
    else if node.className?
      return node.className.split(' ')

  getDefaultOption: (select) ->
    option = select.querySelector('option[selected]')
    if option?
      return option
    else
      # IE8
      for o,i in select.options
        if o.defaultSelected
          return o
    return null

  getText: (node) ->
    switch node.nodeType
      when ScribeDOM.ELEMENT_NODE
        return if node.tagName == "BR" then "" else node.textContent or node.innerText or ""
      when ScribeDOM.TEXT_NODE then return node.data or ""
      else return ""

  getWindow: (node) ->
    return node.ownerDocument.defaultView or node.ownerDocument.parentWindow

  hasClass: (node, cssClass) ->
    if node.classList?
      return node.classList.contains(cssClass)
    else if node.className?
      return _.indexOf(ScribeDOM.getClasses(node), cssClass) > -1
    return false

  mergeNodes: (node1, node2) ->
    return node2 if !node1?
    return node1 if !node2?
    this.moveChildren(node1, node2)
    node2.parentNode.removeChild(node2)
    if (node1.tagName == 'OL' || node1.tagName == 'UL') && node1.childNodes.length == 2
      ScribeDOM.mergeNodes(node1.firstChild, node1.lastChild)
    return node1

  moveChildren: (newParent, oldParent) ->
    _.each(_.clone(oldParent.childNodes), (child) ->
      newParent.appendChild(child)
    )

  normalize: (node) ->
    # Credit: Tim Down - http://stackoverflow.com/questions/2023255/node-normalize-crashes-in-ie6
    child = node.firstChild
    while (child)
      if (child.nodeType == 3)
        while ((nextChild = child.nextSibling) && nextChild.nodeType == 3)
          child.appendData(nextChild.data)
          node.removeChild(nextChild)
      child = child.nextSibling

  removeAttributes: (node, exception = []) ->
    exception = [exception] if _.isString(exception)
    _.each(_.clone(node.attributes), (attrNode, value) ->
      node.removeAttribute(attrNode.name) unless _.indexOf(exception, attrNode.name) > -1
    )

  removeClass: (node, cssClass) ->
    return unless ScribeDOM.hasClass(node, cssClass)
    if node.classList?
      return node.classList.remove(cssClass)
    else if node.className?
      classArray = ScribeDOM.getClasses(node)
      classArray.splice(_.indexOf(classArray, cssClass), 1)
      node.className = classArray.join(' ')

  removeNode: (node) ->
    node.parentNode?.removeChild(node)

  resetSelect: (select) ->
    option = ScribeDOM.getDefaultOption(select)
    if option?
      option.selected = true
      ScribeDOM.triggerEvent(select, 'change')
    else
      select.selectedIndex = null

  setText: (node, text) ->
    switch node.nodeType
      when ScribeDOM.ELEMENT_NODE
        if node.textContent?
          node.textContent = text
        else
          node.innerText = text
      when ScribeDOM.TEXT_NODE then node.data = text
      else return # Noop

  switchTag: (node, newTag) ->
    return if node.tagName == newTag
    newNode = node.ownerDocument.createElement(newTag)
    this.moveChildren(newNode, node)
    node.parentNode.replaceChild(newNode, node)
    newNode.className = node.className if node.className
    newNode.id = node.id if node.id
    return newNode

  toggleClass: (node, className, state) ->
    state = !ScribeDOM.hasClass(node, className) unless state?
    if state
      ScribeDOM.addClass(node, className)
    else
      ScribeDOM.removeClass(node, className)

  triggerEvent: (elem, eventName, bubble, cancels) ->
    if elem.ownerDocument.createEvent
      evt = elem.ownerDocument.createEvent("HTMLEvents")
      evt.initEvent(eventName, bubble, cancels)
      elem.dispatchEvent(evt)
    else
      elem.fireEvent("on#{eventName}", cancels)

  unwrap: (node) ->
    ret = node.firstChild
    next = node.nextSibling
    _.each(_.clone(node.childNodes), (child) ->
      node.parentNode.insertBefore(child, next)
    )
    node.parentNode.removeChild(node)
    return ret

  wrap: (wrapper, node) ->
    node.parentNode.insertBefore(wrapper, node)
    wrapper.appendChild(node)
    return wrapper

module.exports = ScribeDOM


describe('Editor', ->
  template = '

        ab
        cd

      <s>ef</s>
      gh

ij

kl

  '

  describe('insertAt empty', ->
    insertSuite = new ScribeInsertTestSuite({ initial: '

' })
    insertSuite.run()
  )

  describe('insertAt', ->
    insertSuite = new ScribeInsertTestSuite({ initial: template })
    insertSuite.run()
  )

  describe('deleteAt', ->
    deleteSuite = new ScribeDeleteTestSuite({ initial: template })
    deleteSuite.run()
  )

  describe('formatAt', ->
    formatSuite = new ScribeFormatTestSuite({ initial: template })
    formatSuite.run()
  )
)


#mocha
#format-container
  button.bold Bold
  button.link Link
  select.size
    option(value='small') Small
    option(value='normal', selected=true) Normal
    option(value='large') Large
    option(value='huge') Huge
#test-container
#expected-container


require 'colorize'
require 'debugger'
gem "minitest"
require 'minitest/autorun'
require 'minitest/pride'
require_relative '../lib/scribe_driver'
require_relative '../lib/webdriver_adapter'
require_relative '../lib/minitest/focus'

describe "Insert" do
  include ScribeDriver

  before do
    setup_test_suite
  end

  after do
    @driver.quit
  end

  def apply_delta(delta, err_msg)
    ScribeDriver::JS.set_current_delta(delta)
    @adapter.apply_delta(delta)
    actual_delta, success = ScribeDriver::JS.check_consistency().values_at("actual_delta", "success")
    unless success
      doc_delta = ScribeDriver::JS.get_as_str("docDelta")
      cur_delta = ScribeDriver::JS.get_as_str("currentDelta")
      expected_delta = ScribeDriver::JS.get_expected_as_str
      doc_delta = JSON.pretty_generate(JSON.parse(doc_delta))
      cur_delta = JSON.pretty_generate(JSON.parse(cur_delta))
      expected_delta = JSON.pretty_generate(JSON.parse(expected_delta))
      actual_delta = JSON.pretty_generate(JSON.parse(actual_delta))
      error_details = "#{'doc_delta: '.light_cyan + doc_delta},\n"
      error_details << "#{'cur_delta: '.light_cyan + cur_delta},\n"
      error_details << "#{'expected_delta: '.light_cyan + expected_delta},\n"
      error_details << "#{'actual: '.light_cyan + actual_delta}"
      err_msg << "\n#{error_details}"
    end
    success.must_equal true, err_msg
  end

  def insert_at_every_position(text)
    ScribeDriver::JS.execute_js("window.ScribeDriver.resetScribe()")
    @editor = @driver.find_element(:class, "editor")
    @adapter = WebdriverAdapter.new @driver, @editor
    @adapter.focus()
    ScribeDriver::JS.set_doc_delta
    @adapter.doc_length = ScribeDriver::JS.get_doc_length
    doc_length = ScribeDriver::JS.get_doc_length - 1 # - 1 accounts for phantom newline
    (0...doc_length).each do |insert_at|
      cur_length = ScribeDriver::JS.get_doc_length
      delta = ScribeDriver::JS.make_insert_delta(cur_length, @adapter.cursor_pos + 1, text, {})
      apply_delta(delta, "Failed inserting #{text} at index #{insert_at}")
      ScribeDriver::JS.set_doc_delta
    end
  end

  def run_insert_test(initial, insert_delta, err_msg)
    reset_scribe initial
    apply_delta insert_delta, err_msg
  end

  it "should insert a single character at every position" do
    insert_at_every_position "a"
  end

  it "should insert multiple characters at every position" do
    insert_at_every_position "abc"
  end

  it "should insert multiline characters at every position" do
    insert_at_every_position "abc\\ndef\\nghi"
  end

  it "should insert a newline into the empty document" do
    start_delta = { "startLength" => 0,
                    "endLength" => 1,
                    "ops" => [{ "value" => "\n", "attributes" => {}}]}

    current_delta = { "startLength" => 1,
                      "endLength" => 2,
                      "ops" => [{ "value" => "\n", "attributes" => {} },
                                { "start" => 0, "end" => 1}]}
    run_insert_test start_delta, current_delta, "Inserting \n at start of empty document fails."
  end

  it "should append a newline to nonempty document" do
    start_delta = { "startLength" => 0,
                    "endLength" => 4,
                    "ops" => [{ "value" => "abc\n", "attributes" => {}}]}

    current_delta = { "startLength" => 4,
                      "endLength" => 5,
                      "ops" => [{ "start" => 0, "end" => 3, "attributes" => {} },
                                { "value" => "\n", "attributes" => {} },
                                { "start" => 3, "end" => 4, "attributes" => {} }]}

    run_insert_test start_delta, current_delta, "Inserting \n at start of empty document fails."
  end

  describe "tab" do
    it "should indent text when tab is prepended" do
      start_delta = { "startLength" => 0,
                      "endLength" => 4,
                      "ops" => [{ "value" => "abc\n", "attributes" => {}}]}

      current_delta = { "startLength" => 4,
                        "endLength" => 5,
                        "ops" => [{ "value" => "\t", "attributes" => {}},
                                  { "start" => 0, "end" => 4}]}
      run_insert_test start_delta, current_delta, "Failed inserting tab at 0th index."
    end

    it "should indent highlighted lines" do
      start_delta = { "startLength" => 0,
                      "endLength" => 6,
                      "ops" => [{ "value" => "1\n2\n3\n", "attributes" => {}}]}
      reset_scribe start_delta

      @adapter.move_cursor 0
      @adapter.highlight 3
      @editor.send_keys :tab
      expected_delta = { "startLength" => 0,
                         "endLength" => 8,
                         "ops" => [{ "value" => "\t1\n\t2\n3\n", "attributes" => {}}]}

      assert ScribeDriver::JS.editor_delta_equals(expected_delta), "Indenting multiple lines with tab failed."
    end

    it "should prepend a tab followed by more text" do
      start_delta = { "startLength" => 0,
                      "endLength" => 4,
                      "ops" => [{ "value" => "abc\n", "attributes" => {}}]}

      current_delta = { "startLength" => 4,
                        "endLength" => 8,
                        "ops" => [{ "value" => "\tabc", "attributes" => {}},
                                  { "start" => 0, "end" => 4}]}
      run_insert_test start_delta, current_delta, "Failed inserting tab + text at 0th index."
    end

    it "should append a tab" do
      start_delta = { "startLength" => 0,
                      "endLength" => 4,
                      "ops" => [{ "value" => "abc\n", "attributes" => {}}]}

      current_delta = { "startLength" => 4,
                        "endLength" => 5,
                        "ops" => [{ "start" => 0, "end" => 3, "attributes" => {}},
                                  { "value" => "\t", "attributes" => {}},
                                  { "start" => 3, "end" => 4}]}

      run_insert_test start_delta, current_delta, "Failed appending tab."
    end

    it "should append a tab followed by more text" do
      start_delta = { "startLength" => 0,
                      "endLength" => 4,
                      "ops" => [{ "value" => "abc\n", "attributes" => {}}]}

      current_delta = { "startLength" => 4,
                        "endLength" => 8,
                        "ops" => [{ "start" => 0, "end" => 3, "attributes" => {}},
                                  { "value" => "\tdef", "attributes" => {}},
                                  { "start" => 3, "end" => 4}]}

      run_insert_test start_delta, current_delta, "Failed appending tab."
    end
  end
end


class WebdriverAdapter

  attr_accessor :doc_length, :cursor_pos

  def initialize(driver, editor)
    @cursor_pos = 0
    @driver = driver
    @editor = editor
    @doc_length = 1 # Initial val of 1 due to Scribe's "phantom" newline
  end

  # Assumes delta will contain only one document modifying op
  def apply_delta(delta, toolbar_only = false)
    index = 0
    delta['ops'].each_with_index do |op, i|
      if op['value']
        if !op['attributes'].empty?
          click_button_from_toolbar(op['attributes'].keys[0])
        end
        while op and op['value']
          insert_at index, op['value']
          i += 1
          index += op['value'].length
          op = delta['ops'][i]
        end
        break
      elsif op['start'] > index
        delete_length = op['start'] - index
        delete_at(index, delete_length)
        break
      elsif !op['attributes'].empty?
        length = op['end'] - op['start']
        format_at(index, length, op['attributes'], toolbar_only)
        break
      else
        index += op['end'] - op['start']
      end
    end
  end

  def copy(index, length)
    move_cursor(index)
    highlight(length)
    @driver.action.key_down(@@cmd_modifier).send_keys("c").key_up(@@cmd_modifier).perform
    remove_highlighting
  end

  def paste(index)
    move_cursor(index)
    @driver.action.key_down(@@cmd_modifier).send_keys("v").key_up(@@cmd_modifier).perform
  end

  def undo
    @driver.action.key_down(@@cmd_modifier).send_keys("z").key_up(@@cmd_modifier).perform
  end

  def redo
    @driver.action.key_down(@@cmd_modifier).send_keys("Z").key_up(@@cmd_modifier).perform
  end

  def insert_at(index, text)
    move_cursor(index)
    type_text(text)
    move_cursor index + text.length
  end

  def delete_at(index, length)
    move_cursor(index)
    delete(length)
  end

  def format_at(index, length, attributes, toolbar_only = false)
    move_cursor(index)
    highlight(length)
    attributes.each do |attr, val|
      format(attr, val, toolbar_only)
    end
    remove_highlighting
  end

  def remove_highlighting
    @editor.send_keys [] # Workaround for an issue in Ruby bindings.
    @editor.send_keys [:arrow_right]
    sleep 1
  end

  def jump_to_start
    os = WebdriverAdapter.os()
    if os == :windows or os == :linux
      key = :home
    elsif os == :macosx
      key = :arrow_up
    end
    @driver.action.key_down(@@cmd_modifier).send_keys(key).key_up(@@cmd_modifier).perform
  end

  def move_cursor(dest_index)
    if dest_index < 0 or dest_index > doc_length
      raise IndexError, "Invalid dest_index #{dest_index} for doc_length #{doc_length}"
    end
    distance = (@cursor_pos - dest_index).abs
    if dest_index == 0
      jump_to_start()
    elsif @cursor_pos > dest_index
      distance.times do @editor.send_keys(:arrow_left) end
    else
      distance.times do @editor.send_keys(:arrow_right) end
    end
    @cursor_pos = dest_index
  end

  def highlight(length)
    keys = (0...length).to_a.map! { :arrow_right }
    @cursor_pos += length
    @driver.action.key_down(:shift).send_keys(*keys).key_up(:shift).perform
    sleep 1
  end

  def delete(length)
    highlight(length)
    @editor.send_keys(:delete)
    @doc_length -= length
    @cursor_pos -= length
  end

  def split_text(text)
    keys = []
    cur = ""
    text.each_char { |c|
      if c == "\n"
        keys << cur if cur.length > 0
        cur = ""
        keys << :enter
      elsif c == " "
        keys << cur if cur.length > 0
        cur = ""
        keys << :space
      else
        cur << c
      end
    }
    keys << cur if cur.length > 0
    return keys
  end

  def type_text(text)
    keys = split_text(text)
    if @cursor_pos == 0 && @doc_length == 0 && @driver.browser == :firefox
      # Hack to workaround inexplicable firefox behavior in which it appends a
      # newline if you append to the end of the empty document.
      @editor.send_keys(keys)
      @editor.send_keys([:arrow_down, :delete])
    else
      @editor.send_keys keys
    end
    len = keys.reduce(0) do |len, elem|
      if elem.is_a?(String) then len + elem.length else len + 1 end
    end
    @cursor_pos += len
    @doc_length += len
  end

  def click_button_from_toolbar(button_class)
    @driver.switch_to.default_content
    button = @driver.execute_script("return $('#editor-toolbar > .#{button_class}')[0]")
    button.click()
    @driver.switch_to.frame(@driver.find_element(:tag_name, "iframe"))
  end

  def select_from_dropdown(dropdown_class, value)
    @driver.switch_to.default_content
    dropdown = @driver.execute_script("return $('#editor-toolbar > .#{dropdown_class}')[0]")
    Selenium::WebDriver::Support::Select.new(dropdown).select_by(:value, value)
    @driver.switch_to.frame(@driver.find_element(:tag_name, "iframe"))
  end

  def format(format, value, toolbar_only = false)
    case format
    when /bold|italic|underline/
      if Random.rand() < 0.5 && !toolbar_only
        @driver.action.key_down(@@cmd_modifier).send_keys(format[0]).key_up(@@cmd_modifier).perform
      else
        click_button_from_toolbar(format)
      end
    when /link|strike/
      click_button_from_toolbar(format)
    when /font-name|font-size|back-color|fore-color/
      select_from_dropdown(format, value)
    else
      raise "Unknown formatting op: #{format}"
    end
    self.focus # Need to reset focus to the editor after clicking toolbar
  end

  def focus
    @editor.send_keys ""
  end

  def self.os
    host_os = RbConfig::CONFIG['host_os']
    case host_os
    when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
      return :windows
    when /darwin|mac os/
      return :macosx
    when /linux/
      return :linux
    when /solaris|bsd/
      return :unix
    else
      raise Error::WebDriverError, "unknown os: #{host_os.inspect}"
    end
  end

  def self.os_to_modifier
    case self.os()
    when :windows
      return :control
    when :linux
      return :control
    when :macosx
      return :command
    end
  end
  @@cmd_modifier = WebdriverAdapter.os_to_modifier()
end


require_relative '../lib/scribe_driver'

describe "Test Copy Paste" do
  include ScribeDriver
  before do
    setup_test_suite
    start_delta = { "startLength" => 0,
                    "endLength" => 1,
                    "ops" => [{ "value" => "\n", "attributes" => {}}]}
    reset_scribe start_delta
  end

  after do
    @driver.quit
  end

  it "should copy and paste plain text" do
    @adapter.type_text "abc"
    @adapter.copy(0, 3)
    # XXX: Adapter's paste doesn't work yet
    @adapter.paste(3)
  end

  # it "should copy and paste newlines" do
  # end

  # it "should copy and paste formatted text" do
  # end
end


if event.stopPropagation?
event.stopPropagation()
else
event.cancelBubble = true

ScribeDOM         = require('../dom')
ScribeKeyboard    = require('../keyboard')
ScribeRange       = require('../range')
ScribeLinkTooltip = require('./link-tooltip')
ScribeUtils       = require('../utils')

_findInput = (format) ->
  selector = ".#{format}"
  if _.indexOf(ScribeToolbar.formats.SELECT, format) > -1
    selector = 'select' + selector
  input = @container.querySelector(selector)

_initFormats = ->
  _.each(ScribeToolbar.formats, (formats, formatGroup) =>
    _.each(formats, (format) =>
      input = _findInput.call(this, format)
      return unless input?
      return @editor.theme.addModule('link-tooltip', { button: input }) if format == 'link'
      return if format == 'link'
      eventName = if formatGroup == 'SELECT' then 'change' else 'click'
      ScribeDOM.addEventListener(input, eventName, =>
        return if @triggering
        value = if input.tagName == 'SELECT' then input.options[input.selectedIndex].value else !ScribeDOM.hasClass(input, 'active')
        range = @editor.getSelection()
        if range
          @editor.setSelection(range) if ScribeUtils.isIE(9)
          range.format(format, value, { source: 'user' })
        activeFormats = {}
        activeFormats[format] = value
        this.updateActive(activeFormats)
      )
    )
  )
  _.each(['BOLD', 'ITALIC', 'UNDERLINE'], (key) =>
    @editor.keyboard.addHotkey(ScribeKeyboard.hotkeys[key], =>
      activeFormats = {}
      input = _findInput.call(this, key.toLowerCase())
      activeFormats[key.toLowerCase()] = !ScribeDOM.hasClass(input, 'active') if input?
      this.updateActive(activeFormats)
    )
  )

class ScribeToolbar
  @DEFAULTS:
    container: null     # required

  @formats:
    BUTTON: ['bold', 'italic', 'strike', 'underline', 'link', 'indent', 'outdent']
    SELECT: ['back-color', 'fore-color', 'font-name', 'font-size']

  constructor: (@editor, options = {}) ->
    @options = _.defaults(options, ScribeToolbar.DEFAULTS)
    @container = if _.isString(@options.container) then document.getElementById(@options.container) else @options.container
    _initFormats.call(this)
    @editor.on(@editor.constructor.events.POST_EVENT, (eventName) =>
      return unless eventName == @editor.constructor.events.API_TEXT_CHANGE or eventName == @editor.constructor.events.USER_TEXT_CHANGE or eventName == @editor.constructor.events.SELECTION_CHANGE
      this.updateActive()
    )

  updateActive: (activeFormats = {}) ->
    range = @editor.getSelection()
    _.each(@container.querySelectorAll('.active'), (button) =>
      ScribeDOM.removeClass(button, 'active')
    )
    _.each(@container.querySelectorAll('select'), (select) =>
      ScribeDOM.resetSelect(select)
    )
    return unless range?
    @triggering = true
    _.each(_.extend(range.getFormats(), activeFormats), (value, key) =>
      if value
        elem = _findInput.call(this, key)
        return unless elem?
        if elem.tagName == 'SELECT'
          value = '' if _.isArray(value)
          elem.value = value
          ScribeDOM.triggerEvent(elem, 'change')
        else
          ScribeDOM.addClass(elem, 'active')
    )
    @triggering = false

module.exports = ScribeToolbar


ScribeDOM       = require('../dom')
ScribeKeyboard  = require('../keyboard')
ScribePosition  = require('../position')
ScribeRange     = require('../range')

enterEditMode = (url) ->
  url = normalizeUrl(url)
  ScribeDOM.addClass(@tooltip, 'editing')
  @tooltipInput.focus()
  @tooltipInput.value = url

exitEditMode = ->
  if @savedLink? or ScribeDOM.getText(@tooltipLink) != @tooltipInput.value
    url = normalizeUrl(@tooltipInput.value)
    @tooltipLink.href = url
    ScribeDOM.setText(@tooltipLink, url)
    if @savedLink?
      @savedLink.href = url
      @savedLink = null
    else
      formatLink.call(this, @tooltipInput.value)
  ScribeDOM.removeClass(@tooltip, 'editing')

formatLink = (value) ->
  @editor.setSelection(@savedRange, true)
  @savedRange.format('link', value, { source: 'user' })

hideTooltip = ->
  @tooltip.style.left = '-10000px'

initListeners = ->
  ScribeDOM.addEventListener(@editor.root, 'mouseup', (event) =>
    link = event.target
    while link? and link.tagName != 'DIV' and link.tagName != 'BODY'
      if link.tagName == 'A'
        url = normalizeUrl(link.href)
        @tooltipLink.href = url
        ScribeDOM.setText(@tooltipLink, url)
        ScribeDOM.removeClass(@tooltip, 'editing')
        showTooltip.call(this, link.getBoundingClientRect())
        @savedLink = link
        return
      link = link.parentNode
    hideTooltip.call(this)
  )
  ScribeDOM.addEventListener(@options.button, 'click', =>
    @savedRange = @editor.getSelection()
    return unless @savedRange? and !@savedRange.isCollapsed()
    if ScribeDOM.hasClass(@options.button, 'active')
      formatLink.call(this, false)
      hideTooltip.call(this)
    else
      url = normalizeUrl(@savedRange.getText())
      if /\w+\.\w+/.test(url)
        @editor.root.focus()
        formatLink.call(this, url)
      else
        ScribeDOM.addClass(@tooltip, 'editing')
        showTooltip.call(this, @editor.selection.getDimensions())
        enterEditMode.call(this, url)
  )
  ScribeDOM.addEventListener(@tooltipChange, 'click', =>
    enterEditMode.call(this, ScribeDOM.getText(@tooltipLink))
  )
  ScribeDOM.addEventListener(@tooltipDone, 'click', =>
    exitEditMode.call(this)
  )
  ScribeDOM.addEventListener(@tooltipInput, 'keyup', (event) =>
    exitEditMode.call(this) if event.which == ScribeKeyboard.keys.ENTER
  )

initTooltip = ->
  @tooltip = @options.button.ownerDocument.createElement('div')
  @tooltip.id = 'link-tooltip'
  @tooltip.innerHTML =
   'Visit URL:

    <input class="input" type="text">
    -
    Change
    Done'
  @tooltipLink = @tooltip.querySelector('.url')
  @tooltipInput = @tooltip.querySelector('.input')
  @tooltipChange = @tooltip.querySelector('.change')
  @tooltipDone = @tooltip.querySelector('.done')
  @editor.renderer.addStyles(
    '#link-tooltip': {
      'background-color': '#fff'
      'border': '1px solid #000'
      'height': '23px'
      'padding': '5px 10px'
      'position': 'absolute'
      'white-space': 'nowrap'
    }
    '#link-tooltip a': {
      'cursor': 'pointer'
      'text-decoration': 'none'
    }
    '#link-tooltip > a, #link-tooltip > span': {
      'display': 'inline-block'
      'line-height': '23px'
    }
    '#link-tooltip .input'          : { 'display': 'none', 'width': '170px' }
    '#link-tooltip .done'           : { 'display': 'none' }
    '#link-tooltip.editing .input'  : { 'display': 'inline-block' }
    '#link-tooltip.editing .done'   : { 'display': 'inline-block' }
    '#link-tooltip.editing .url'    : { 'display': 'none' }
    '#link-tooltip.editing .change' : { 'display': 'none' }
  )
  @tooltip.style.left = '-10000px'
  @editor.renderer.addContainer(@tooltip)

normalizeUrl = (url) ->
  url = 'http://' + url unless /^https?:\/\//.test(url)
  url = url.slice(0, url.length - 1) if url.slice(url.length - 1) == '/' # Remove trailing slash to standardize between browsers
  return url

showTooltip = (target, subjectDist = 5) ->
  tooltip = @tooltip.getBoundingClientRect()
  tooltipHeight = tooltip.bottom - tooltip.top
  tooltipWidth = tooltip.right - tooltip.left
  limit = @editor.root.getBoundingClientRect()
  left = Math.max(limit.left, target.left + (target.right-target.left)/2 - tooltipWidth/2)
  if left + tooltipWidth > limit.right and limit.right - tooltipWidth > limit.left
    left = limit.right - tooltipWidth
  top = target.bottom + subjectDist
  if top + tooltipHeight > limit.bottom and target.top - tooltipHeight - subjectDist > limit.top
    top = target.top - tooltipHeight - subjectDist
  @tooltip.style.left = left + 'px'
  @tooltip.style.top = (top + (@tooltip.offsetTop-tooltip.top)) + 'px'

class ScribeLinkTooltip
  @DEFAULTS:
    button: null      # Required

  constructor: (@editor, options = {}) ->
    @options = _.defaults(options, ScribeLinkTooltip.DEFAULTS)
    initTooltip.call(this)
    initListeners.call(this)

module.exports = ScribeLinkTooltip


ScribeDOM       = require('./dom')
ScribeLine      = require('./line')
ScribePosition  = require('./position')
ScribeRange     = require('./range')

_initDeletes = ->
  _.each([ScribeKeyboard.keys.DELETE, ScribeKeyboard.keys.BACKSPACE], (key) =>
    this.addHotkey(key, =>
      # Prevent deleting if editor is already blank (browser quirk fix)
      return @editor.getLength() > 1
    )
  )

_initHotkeys = ->
  this.addHotkey(ScribeKeyboard.hotkeys.OUTDENT, (range) =>
    _onTab.call(this, range, true)
    return false
  )
  this.addHotkey(ScribeKeyboard.hotkeys.INDENT, (range) =>
    _onTab.call(this, range, false)
    return false
  )
  _.each(['bold', 'italic', 'underline'], (format) =>
    this.addHotkey(ScribeKeyboard.hotkeys[format.toUpperCase()], (range) =>
      this.toggleFormat(range, format)
      return false
    )
  )

_initListeners = ->
  ScribeDOM.addEventListener(@editor.root, 'keydown', (event) =>
    if @hotkeys[event.which]?
      prevent = false
      _.each(@hotkeys[event.which], (hotkey) =>
        return if hotkey.meta? and (event.metaKey != hotkey.meta and event.ctrlKey != hotkey.meta)
        return if hotkey.shift? and event.shiftKey != hotkey.shift
        @editor.selection.update(true)
        selection = @editor.getSelection()
        return unless selection?
        prevent = hotkey.callback.call(null, selection) == false or prevent
      )
    return !prevent
  )

_onTab = (range, shift = false) ->
  # Behavior according to Google Docs + Word
  # When tab on one line, regardless if shift is down, delete selection and insert a tab
  # When tab on multiple lines, indent each line if possible, outdent if shift is down
  lines = range.getLines()
  if lines.length > 1
    index = ScribePosition.getIndex(lines[0].node)
    start = range.start.index + (if shift then -1 else 1)
    offsetChange = 0
    _.each(lines, (line) =>
      if !shift
        @editor.insertAt(index, '\t', {}, { source: 'user' })
        offsetChange += 1
      else if line.leaves.first.text[0] == '\t'
        @editor.deleteAt(index, 1, { source: 'user' })
        offsetChange -= 1
      else if line == lines[0]
        start = range.start.index
      index += line.length
    )
    end = range.end.index + offsetChange
    @editor.setSelection(new ScribeRange(@editor, start, end))
  else
    range.deleteContents({ source: 'user' })
    range.insertContents(0, "\t", {}, { source: 'user' })
    @editor.setSelection(new ScribeRange(@editor, range.start.index + 1, range.start.index + 1))

class ScribeKeyboard
  @keys:
    BACKSPACE : 8
    TAB       : 9
    ENTER     : 13
    LEFT      : 37
    UP        : 38
    RIGHT     : 39
    DOWN      : 40
    DELETE    : 46

  @hotkeys:
    BOLD:       { key: 'B',       meta: true }
    INDENT:     { key: @keys.TAB, shift: false }
    ITALIC:     { key: 'I',       meta: true }
    OUTDENT:    { key: @keys.TAB, shift: true }
    UNDERLINE:  { key: 'U',       meta: true }
    UNDO:       { key: 'Z',       meta: true, shift: false }
    REDO:       { key: 'Z',       meta: true, shift: true }
    SELECT_ALL: { key: 'A',       meta: true }

  @NAVIGATION: [@keys.UP, @keys.DOWN, @keys.LEFT, @keys.RIGHT]

  constructor: (@editor) ->
    @hotkeys = {}
    _initListeners.call(this)
    _initHotkeys.call(this)
    _initDeletes.call(this)

  addHotkey: (hotkey, callback) ->
    hotkey = if _.isObject(hotkey) then _.clone(hotkey) else { key: hotkey }
    hotkey.key = hotkey.key.toUpperCase().charCodeAt(0) if _.isString(hotkey.key)
    hotkey.callback = callback
    @hotkeys[hotkey.key] ?= []
    @hotkeys[hotkey.key].push(hotkey)

  indent: (selection, increment) ->
    lines = selection.getLines()
    applyIndent = (line, format) =>
      if increment
        indent = if _.isNumber(line.formats[format]) then line.formats[format] else (if line.formats[format] then 1 else 0)
        indent += increment
        indent = Math.min(Math.max(indent, ScribeLine.MIN_INDENT), ScribeLine.MAX_INDENT)
      else
        indent = false
      index = Position.getIndex(line.node, 0)
      @editor.formatAt(index, 0, format, indent)

    _.each(lines, (line) =>
      if line.formats.bullet?
        applyIndent(line, 'bullet')
      else if line.formats.list?
        applyIndent(line, 'list')
      else
        applyIndent(line, 'indent')
    )

  toggleFormat: (range, format) ->
    formats = range.getFormats()
    value = !formats[format]
    range.format(format, value, { source: 'user' })

module.exports = ScribeKeyboard


replay_file=$(shell find tests/webdriver/fuzzer_output/fails -type f -exec stat -f "%m %N" {} \; | sort -n | tail -1 | cut -f2- -d" ")

coverage:
	@./node_modules/.bin/karma start tests/karma/unit.conf.coffee --browsers Chrome --reporters coverage

test:
	@./node_modules/.bin/karma start tests/karma/unit.conf.coffee --browsers PhantomJS

test-all:
	@./node_modules/.bin/karma start tests/karma/all.conf.coffee

test-karma:
	@./node_modules/.bin/karma start tests/karma/unit.conf.coffee --browsers PhantomJS --no-single-run

test-remote:
	@./node_modules/.bin/karma start tests/karma/browserstack.conf.coffee

test-unit:
	@./node_modules/.bin/karma start tests/karma/unit.conf.coffee

webdriver-fuzzer-chrome:
	@ruby tests/webdriver/fuzzer.rb chrome

webdriver-fuzzer-firefox:
	@ruby tests/webdriver/fuzzer.rb firefox

webdriver-fuzzer-chrome-replay:
	@ruby tests/webdriver/fuzzer.rb chrome $(replay_file)

webdriver-fuzzer-firefox-replay:
	@ruby tests/webdriver/fuzzer.rb firefox $(replay_file)

webdriver-unit-chrome:
	@ruby tests/webdriver/unit/unit_runner.rb chrome

webdriver-unit-firefox:
	@ruby tests/webdriver/unit/unit_runner.rb firefox


eventName = if formatGroup == 'SELECT' then 'change' else 'click'

value = if input.tagName == 'SELECT' then input.options[input.selectedIndex].value else !ScribeDOM.hasClass(input, 'active')

range.format(format, value, { source: 'user' })

keep keep keep keep keep replace replace keep keep keep keep


# fix for src/format-manager.coffee's ScribeFormat = require('./formats')
ScribeFormat = window.module.exports


ScribeDOM     = require('../dom')
ScribeFormat  = require('../format')
Tandem        = require('tandem-core')

class ScribeAttribution
  DEFAULTS:
    authorId: null
    color: 'blue'
    enabled: false

  constructor: (@editor, options) ->
    @options = _.defaults(options, ScribeAttribution.DEFAULTS)
    @options.authorId or= @editor.id
    @editor.on(@editor.constructor.events.PRE_EVENT, (eventName, delta) =>
      if eventName == @editor.constructor.events.USER_TEXT_CHANGE
        # Add authorship to insert/format
        _.each(delta.ops, (op) =>
          if Tandem.InsertOp.isInsert(op) or _.keys(op.attributes).length > 0
            op.attributes['author'] = @options.authorId
        )
        # Apply authorship to our own editor
        authorDelta = new Tandem.Delta(delta.endLength, [new Tandem.RetainOp(0, delta.endLength)])
        attribute = {}
        attribute['author'] = @options.authorId
        delta.apply((index, text) =>
          _.each(text.split('\n'), (text) ->
            authorDelta = authorDelta.compose(Tandem.Delta.makeRetainDelta(delta.endLength, index, text.length, attribute))
            index += text.length + 1
          )
        , (=>)
        , (index, length, name, value) =>
          authorDelta = authorDelta.compose(Tandem.Delta.makeRetainDelta(delta.endLength, index, length, attribute))
        )
        @editor.applyDelta(authorDelta, { silent: true })
    )
    @editor.doc.formatManager.addFormat('author', new ScribeFormat.Class(@editor.renderer.root, 'author'))
    this.addAuthor(@options.authorId, @options.color)
    this.enable() if @options.enabled

  addAuthor: (id, color) ->
    styles = {}
    styles[".editor.attribution .author-#{id}"] = { "background-color": "#{color}" }
    @editor.renderer.addStyles(styles)

  enable: ->
    ScribeDOM.addClass(@editor.root, 'attribution')

  disable: ->
    ScribeDOM.removeClass(@editor.root, 'attribution')

module.exports = ScribeAttribution


ScribeDOM   = require('./dom')

ieVersion = do ->
  matchVersion = navigator.userAgent.match(/MSIE [0-9\.]+/)
  return if matchVersion? then parseInt(matchVersion[0].slice("MSIE".length)) else null

ScribeUtils =
  BLOCK_TAGS: [
    'ADDRESS'
    'BLOCKQUOTE'
    'DD'
    'DIV'
    'DL'
    'H1', 'H2', 'H3', 'H4', 'H5', 'H6'
    'LI'
    'OL'
    'P'
    'PRE'
    'TABLE'
    'TBODY'
    'TD'
    'TFOOT'
    'TH'
    'THEAD'
    'TR'
    'UL'
  ]

  findAncestor: (node, checkFn) ->
    while node? && !checkFn(node)
      node = node.parentNode
    return node

  findClosestPoint: (point, list, prepFn = ->) ->    # Using Euclidien distance
    point = prepFn.call(null, point)
    point = [point] if !_.isArray(point)
    closestDist = Infinity
    closestValue = false
    for key,coords of list
      coords = prepFn.call(null, coords)
      coords = [coords] if !_.isArray(coords)
      dist = _.reduce(coords, (dist, coord, i) ->
        dist + Math.pow(coord - point[i], 2)
      , 0)
      dist = Math.sqrt(dist)
      return key if dist == 0
      if dist < closestDist
        closestDist = dist
        closestValue = key
    return closestValue

  findDeepestNode: (node, offset) ->
    if node.firstChild?
      for child in _.clone(node.childNodes)
        length = ScribeUtils.getNodeLength(child)
        if offset < length
          return ScribeUtils.findDeepestNode(child, offset)
        else
          offset -= length
      return ScribeUtils.findDeepestNode(child, offset + length)
    else
      return [node, offset]

  getChildAtOffset: (node, offset) ->
    child = node.firstChild
    length = ScribeUtils.getNodeLength(child)
    while child?
      break if offset < length
      offset -= length
      child = child.nextSibling
      length = ScribeUtils.getNodeLength(child)
    unless child?
      child = node.lastChild
      offset = ScribeUtils.getNodeLength(child)
    return [child, offset]

  getNodeLength: (node) ->
    return 0 unless node?
    if node.nodeType == ScribeDOM.ELEMENT_NODE
      return _.reduce(node.childNodes, (length, child) ->
        return length + ScribeUtils.getNodeLength(child)
      , if ScribeUtils.isLineNode(node) then 1 else 0)
    else if node.nodeType == ScribeDOM.TEXT_NODE
      return ScribeDOM.getText(node).length
    else
      return 0

  isBlock: (node) ->
    return _.indexOf(ScribeUtils.BLOCK_TAGS, node.tagName, true) > -1

  isEmptyDoc: (root) ->
    firstLine = root.firstChild
    return true if firstLine == null
    return true if firstLine.firstChild == null
    return true if firstLine.firstChild == firstLine.lastChild and firstLine.firstChild.tagName == 'BR'
    return false

  # We'll take a leap of faith that IE11 is good enough...
  isIE: (maxVersion = 10) ->
    return ieVersion? and maxVersion >= ieVersion

  isLineNode: (node) ->
    return node?.parentNode? and ScribeDOM.hasClass(node.parentNode, 'editor') and ScribeUtils.isBlock(node)

  removeFormatFromSubtree: (subtree, format) ->
    if format.matchContainer(subtree)
      subtree = ScribeDOM.unwrap(subtree)
    _.each(subtree.childNodes, (child) ->
      ScribeUtils.removeFormatFromSubtree(child, format)
    )
    return subtree

  # Firefox needs splitBefore, not splitAfter like it used to be, see doc/selection
  splitBefore: (node, root) ->
    return false if node == root or node.parentNode == root
    parentNode = node.parentNode
    parentClone = parentNode.cloneNode(false)
    parentNode.parentNode.insertBefore(parentClone, parentNode)
    while node.previousSibling?
      parentClone.insertBefore(node.previousSibling, parentClone.firstChild)
    ScribeUtils.splitBefore(parentNode, root)

  splitNode: (node, offset, force = false) ->
    # Check if split necessary
    nodeLength = ScribeUtils.getNodeLength(node)
    offset = Math.max(0, offset)
    offset = Math.min(offset, nodeLength)
    return [node.previousSibling, node, false] unless force or offset != 0
    return [node, node.nextSibling, false] unless force or offset != nodeLength
    if node.nodeType == ScribeDOM.TEXT_NODE
      after = node.splitText(offset)
      return [node, after, true]
    else
      left = node
      right = node.cloneNode(false)
      node.parentNode.insertBefore(right, left.nextSibling)
      [child, offset] = ScribeUtils.getChildAtOffset(node, offset)
      [childLeft, childRight] = ScribeUtils.splitNode(child, offset)
      while childRight != null
        nextRight = childRight.nextSibling
        right.appendChild(childRight)
        childRight = nextRight
      return [left, right, true]

  traversePostorder: (root, fn, context = fn) ->
    return unless root?
    cur = root.firstChild
    while cur?
      ScribeUtils.traversePostorder.call(context, cur, fn)
      cur = fn.call(context, cur)
      cur = cur.nextSibling if cur?

  traversePreorder: (root, offset, fn, context = fn, args...) ->
    return unless root?
    cur = root.firstChild
    while cur?
      nextOffset = offset + ScribeUtils.getNodeLength(cur)
      curHtml = cur.innerHTML
      cur = fn.call(context, cur, offset, args...)
      ScribeUtils.traversePreorder.call(null, cur, offset, fn, context, args...)
      if cur? && cur.innerHTML == curHtml
        cur = cur.nextSibling
        offset = nextOffset

  traverseSiblings: (curNode, endNode, fn) ->
    while curNode?
      nextSibling = curNode.nextSibling
      fn(curNode)
      break if curNode == endNode
      curNode = nextSibling

module.exports = ScribeUtils


# Save selection before click is registered

# Prevent from losing focus, needed for bug in mobile Safari


(function() {
  var _ = window._.noConflict();
  var EventEmitter2 = window.EventEmitter2;
  var LinkedList = window.LinkedList;
  var rangy = window.rangy;
  window.rangy = window.LinkedList = window.EventEmitter2 = undefined;


WARNING
===

Please do not publicize this repository in any way. There are a few known documentation gaps and browser quirks we wish to address before publication. Thank you for your patience.

Scribe Rich Text Editor
===

Cross platform rich text editor built with coauthoring in mind.

[![Build Status](https://secure.travis-ci.org/stypi/scribe.png?branch=master)](http://travis-ci.org/stypi/scribe)

How to Use
---

```javascript
var editor = new Scribe.Editor('editor')      // 'editor' is id of dom container
```

Editor API
---

### Text Operations

- editor.getAt(index, length) - get length text starting from index, default to entire document
- editor.insertAt(index, text, formatting) - insert text at index
- editor.deleteAt(index, length) - delete length characters starting from index
- editor.formatAt(index, length, name, value) - apply formatting to length characters starting at index

### Delta operations

See [Tandem](https://github.com/stypi/tandem) for details on deltas.

- editor.applyDelta(delta) - applies delta to editor
- editor.getDelta() - returns delta that describes contents of editor
- editor.setDelta(delta) - sets editor to delta, assuming delta contains only insert operations

### Events

Events names are accessible through Scribe.Editor.events

- editor.on('api-text-change', function(delta))
- editor.on('user-text-change', function(delta))
- editor.on('selection-change', function(range)) - range is the new range of the selection

Toolbar API
---

You can create a toolbar to assist in formatting the editor.

```javascript
var toolbar = new Scribe.Toolbar('toolbar', editor)   // 'toolbar' is the id of dom container
```

Any DOM node with the following classes that is clicked will trigger the corresponding format change:

- bold
- italic
- strike
- underline
- link

Toolbar will listen to selection changes and will add/remove the 'active' class to the corresponding DOM node.

Any DOM node with the following classes that is changed (change DOM event, ex. <select>) will trigger the corresponding format change:

- background - background color
- color - text color
- family - font family
- size - font size

Toolbar will listen to selection changes and will set the corresponding DOM node value to the selected text value.

Local Development
---

### Installation

Install dependencies

    npm install -g grunt-cli
    npm install
    bundle install

### Building

We use grunt to compile coffeescript, stylus, and jade

    grunt

### Version numbers

Until we write a script, version numbers will have to be updated in the following files:

- Gruntfile.coffee
- index.coffee
- package.json

Testing
---

### Unit tests

Visit build/tests/unit.html to run unit tests or run:

    make test

### Browser Testing

You can run the unit tests on all browsers on your machine by running:

    make test-all

You can also test on different operating systems or browsers on Browserstack:

    export BROWSER_STACK_USERNAME=yourusername
    export BROWSER_STACK_ACCESS_KEY=yourapikey
    make test-remote

### Webdriver Testing
For webdriver unit tests, run:

    make webdriver-unit-chrome
    make webdriver-unit-firefox

For the fuzzer, run:

    make webdriver-fuzzer-[browser]
    make webdriver-fuzzer-[browser]-replay _replay_file_

Possible values for ```_browser_``` are ```chrome```, ```internet_explorer```, or ```firefox```.
```_replay_file_``` must be the absolute path to the file the fuzzer creates after a failed run.
This file contains the final edit that caused the fuzzer to fail, so that you can replay the edit until you fix the bug.

In order to run any Webdriver tests against Chrome or IE, you'll need to download
[ChromeDriver](https://code.google.com/p/chromedriver/downloads/list) or
[InternetExplorerDriver](https://code.google.com/p/selenium/downloads/list) and add it to your system's path.
Support for Firefox is built in and requires no special downloads.

The fuzzer currently works against IE 9 & 10, Firefox 19 - 21, and Chrome 26.


ScribeDOM       = require('../dom')
ScribePosition  = require('../position')
ScribeRenderer  = require('../renderer')
ScribeUtils     = require('../utils')

_applyDelta = (delta) ->
  delta.apply((index, text, formatting) =>
    this.shiftCursors(index, text.length, formatting['author'], false)
  , (index, length) =>
    this.shiftCursors(index, -1 * length, null, false)
  , (index, length, name, value) =>
    this.shiftCursors(index, 0, null, false)
  )
  this.update()

_buildCursor = (name, color) ->
  cursor = @container.ownerDocument.createElement('span')
  ScribeDOM.addClass(cursor, 'cursor')
  cursor.innerHTML = @options.template
  cursorFlag = cursor.querySelector('.cursor-flag')
  cursorName = cursor.querySelector('.cursor-name')
  ScribeDOM.setText(cursorName, name)
  cursorCaret = cursor.querySelector('.cursor-caret')
  cursorCaret.style.backgroundColor = cursorName.style.backgroundColor = color
  @container.appendChild(cursor)
  return cursor

_moveCursor = (cursor, referenceNode) ->
  cursor.elem.style.top = referenceNode.offsetTop + 'px'
  cursor.elem.style.left = referenceNode.offsetLeft + 'px'
  cursor.elem.style.height = referenceNode.offsetHeight + 'px'
  if parseInt(cursor.elem.style.top) < parseInt(cursor.elem.style.height)
    ScribeDOM.addClass(cursor.elem, 'top')
  else
    ScribeDOM.removeClass(cursor.elem, 'top')

_updateCursor = (cursor) ->
  @editor.doSilently( =>
    position = new ScribePosition(@editor, cursor.index)
    guide = @container.ownerDocument.createElement('span')
    if !position.leafNode.firstChild?
      ScribeDOM.setText(guide, ScribeDOM.NOBREAK_SPACE)
      # Should only be the case for empty lines
      position.leafNode.parentNode.insertBefore(guide, position.leafNode)
      _moveCursor.call(this, cursor, guide)
    else
      ScribeDOM.setText(guide, ScribeDOM.ZERO_WIDTH_NOBREAK_SPACE)
      [leftText, rightText, didSplit] = ScribeUtils.splitNode(position.leafNode.firstChild, position.offset)
      if rightText?
        rightText.parentNode.insertBefore(guide, rightText)
        _moveCursor.call(this, cursor, guide)
      else if leftText?
        leftText.parentNode.appendChild(guide)
        _moveCursor.call(this, cursor, guide)
    guide.parentNode.removeChild(guide)
    ScribeDOM.normalize(position.leafNode) if didSplit
  )
  cursor.dirty = false

class ScribeMultiCursor
  @DEFAULTS:
    template:
     '

      '
    timeout: 2500

  constructor: (@editor, options = {}) ->
    @options = _.defaults(options, ScribeMultiCursor.DEFAULTS)
    @cursors = {}
    @container = @editor.root.ownerDocument.createElement('div')
    @container.id = 'cursor-container'
    @editor.renderer.addContainer(@container, true)
    @editor.renderer.addStyles({
      '#cursor-container': { 'position': 'absolute', 'z-index': '1000' }
      '.cursor': { 'margin-left': '-1px', 'position': 'absolute' }
      '.cursor-flag':
        'bottom': '100%'
        'position': 'absolute'
        'white-space': 'nowrap'
      '.cursor-name':
        'display': 'inline-block'
        'color': 'white'
        'padding': '2px 8px'
      '.cursor-caret':
        'height': '100%'
        'position': 'absolute'
        'width': '2px'
      '.cursor.hidden .cursor-flag': { 'display': 'none' }
      '.cursor.top > .cursor-flag': { 'bottom': 'auto', 'top': '100%' }
    })
    @editor.renderer.on(ScribeRenderer.events.UPDATE, =>
      _.defer( =>
        @container.style.top = @editor.root.offsetTop + 'px'
        @container.style.left = @editor.root.offsetLeft  + 'px'
      )
    )
    this.initListeners()

  initListeners: ->
    @editor.on(@editor.constructor.events.API_TEXT_CHANGE, (delta) =>
      _applyDelta.call(this, delta)
    ).on(@editor.constructor.events.USER_TEXT_CHANGE, (delta) =>
      _applyDelta.call(this, delta)
    )

  shiftCursors: (index, length, authorId = null, update = true) ->
    _.each(@cursors, (cursor, id) =>
      return unless cursor and (cursor.index > index or cursor.userId == authorId)
      cursor.index += Math.max(length, index - cursor.index)
      cursor.dirty = true
    )
    this.update() if update

  setCursor: (userId, index, name, color, update = true) ->
    cursor = @cursors[userId]
    unless cursor?
      @cursors[userId] = cursor = {
        userId: userId
        index: index
        elem: _buildCursor.call(this, name, color)
      }
    cursor.index = index
    cursor.dirty = true
    ScribeDOM.removeClass(cursor.elem, 'hidden')
    clearTimeout(cursor.timer)
    cursor.timer = setTimeout( =>
      ScribeDOM.addClass(cursor.elem, 'hidden')
      cursor.timer = null
    , @options.timeout)
    _updateCursor.call(this, cursor) if update
    return cursor

  clearCursors: ->
    _.each(_.keys(@cursors), (id) =>
      this.removeCursor(id)
    )
    @cursors = {}

  removeCursor: (userId) ->
    cursor = @cursors[userId]
    cursor.elem.parentNode.removeChild(cursor.elem) of cursor?
    delete @cursors[userId]

  update: ->
    _.each(@cursors, (cursor, id) =>
      _updateCursor.call(this, cursor) if cursor?.dirty
    )

module.exports = ScribeMultiCursor


describe('Range', ->
  describe('Position', ->
    positionTests = new ScribeEditorTest(
      initial: [
        '
1234
'
        '
<s>56</s>78
'
        '

'
      ]
      expected: [0, 1, 2]
    )

    positionTests.run('should correctly initialize position from index',
      checker: (editor) ->
        expectedResults = [
          { text: '12', offset: 0 }
          { text: '12', offset: 1 }
          { text: '34', offset: 0 }
          { text: '34', offset: 1 }
          { text: '34', offset: 2 }
          { text: '56', offset: 0 }
          { text: '56', offset: 1 }
          { text: '78', offset: 0 }
          { text: '78', offset: 1 }
          { text: '78', offset: 2 }
          { text: '', offset: 0 }
        ]
        _.each(expectedResults, (result, i) ->
          position = new ScribePosition(editor, i)
          expect(ScribeDOM.getText(position.leafNode)).to.equal(result.text)
          expect(position.offset).to.equal(result.offset)
        )
    )
  )

  describe('getText', ->
    textTests = new ScribeEditorTest(
      initial:
        '

          abc
          def

            <s>hi</s>

            jk
            lm

            <s>no</s>

          <s>p</s>
          q
          <s>r</s>
          s

'
      expected: [0]
    )
    text = "abcdef\nhijklmno\npqrs"

    textTests.run('should identifiy single node',
      checker: (editor) ->
        _.each(text.split(''), (char, index) ->
          range = new ScribeRange(editor, index, index + 1)
          expect(range.getText()).to.equal(char)
        )
    )

    textTests.run('should identifiy entire document',
      checker: (editor) ->
        range = new ScribeRange(editor, 0, text.length)
        expect(range.getText()).to.equal(text)
    )
  )

  describe('getFormats', ->
    formatTests = new ScribeEditorTest(
      initial:
        '

          123
          456

            <s>78</s>

            90
            12

            <s>34</s>

          <s>5</s>
          6
          <s>7</s>
          8

'
      expected: [0]
      checker: (testEditor, expectedEditor, start, end, text, formats) ->
        range = new ScribeRange(testEditor, start, end)
        expect(range.getText()).to.equal(text)
        expect(range.getFormats()).to.eql(_.extend({}, ScribeLeaf.DEFAULT_FORMATS, formats))
    )

    formatTests.run('inside of node', {}, 1, 2, '2', { bold: true })
    formatTests.run('start of node', {}, 0, 1, '1', { bold: true })
    formatTests.run('end of node', {}, 2, 3, '3', { bold: true })
    formatTests.run('entire node', {}, 0, 3, '123', { bold: true })
    formatTests.run('cursor inside of node', {}, 1, 1, '', { bold: true })
    formatTests.run('cursor at start of node', {}, 0, 0, '', { bold: true })
    formatTests.run('cursor at end of node', {}, 3, 3, '', { italic: true })
    formatTests.run('node at end of document', {}, 19, 20, '8', { underline: true })
    formatTests.run('cursor at end of document', {}, 20, 20, '', { underline: true })
    formatTests.run('part of two nodes', {}, 8, 10, '89', { bold: true })
    formatTests.run('node with preceding newline', {}, 6, 9, '\n78', { bold: true, strike: true })
    formatTests.run('node with trailing newline', {}, 13, 16, '34\n', { bold: true, strike: true })
    formatTests.run('line with preceding and trailing newline', {}, 6, 16, "\n78901234\n", { bold: true })
  )

  describe('getLeafNodes', ->
    leafTests = new ScribeEditorTest(
      initial: [
        '
123456
'
        '
<s>7</s>8<s>9</s>0
'
      ]
      expected: [0, 1]
    )

    leafTests.run('should select a single node at boundaries',
      checker: (editor) ->
        range = new ScribeRange(editor, 0, 3)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(1)
        expect(nodes[0]).to.equal(editor.root.firstChild.firstChild)
    )

    leafTests.run('should select multiple nodes at boundaries',
      checker: (editor) ->
        range = new ScribeRange(editor, 0, 6)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(2)
        expect(nodes[0]).to.equal(editor.root.firstChild.childNodes[0])
        expect(nodes[1]).to.equal(editor.root.firstChild.childNodes[1])
    )

    leafTests.run('should select a single node inside boundaries',
      checker: (editor) ->
        for i in [0..2]
          range = new ScribeRange(editor, i, i+1)
          nodes = range.getLeafNodes()
          expect(nodes.length).to.equal(1)
          expect(nodes[0]).to.equal(editor.root.firstChild.firstChild)
    )

    leafTests.run('should select multipe nodes inside boundaries',
      checker: (editor) ->
        for i in [0..2]
          range = new ScribeRange(editor, i, i+4)
          nodes = range.getLeafNodes()
          expect(nodes.length).to.equal(2)
          expect(nodes[0]).to.equal(editor.root.firstChild.childNodes[0])
          expect(nodes[1]).to.equal(editor.root.firstChild.childNodes[1])
    )

    leafTests.run('should select multiple nodes across lines within boundaries',
      checker: (editor) ->
        range = new ScribeRange(editor, 0, 6)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(2)
        expect(nodes[0]).to.equal(editor.root.firstChild.childNodes[0])
        expect(nodes[1]).to.equal(editor.root.firstChild.childNodes[1])
    )

    leafTests.run('should select multiple nodes across lines outside boundaries',
      checker: (editor) ->
        range = new ScribeRange(editor, 5, 8)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(2)
        expect(nodes[0]).to.equal(editor.root.firstChild.lastChild)
        expect(nodes[1]).to.equal(editor.root.lastChild.firstChild)
    )

    leafTests.run('should select node collapsed',
      checker: (editor) ->
        for i in [0..2]
          range = new ScribeRange(editor, i, i)
          nodes = range.getLeafNodes()
          expect(nodes.length).to.equal(1)
          expect(nodes[0]).to.equal(editor.root.firstChild.firstChild)
    )
  )
)


ScribeColorPicker   = require('./color-picker')
ScribeDefaultTheme  = require('./default')
ScribeDOM           = require('../dom')
ScribePicker        = require('./picker')

class ScribeSnowTheme extends ScribeDefaultTheme
  @COLORS: [
    "#000000", "#e60000", "#ff9900", "#ffff00", "#008A00", "#0066cc", "#9933ff"
    "#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff"
    "#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff"
    "#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2"
    "#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466"
  ]

  constructor: (@editor) ->
    _.defaults(
      styles:
        'div.editor': { 'bottom': '15px', 'top': '15px' }
    , @editor.options.renderer)
    @pickers = []
    @editor.on(@editor.constructor.events.SELECTION_CHANGE, =>
      _.each(@pickers, (picker) ->
        picker.close()
      )
    )
    super

  addModule: (name, options) ->
    switch name
      when 'attribution'  then this.extendAttribution(options)
      when 'link-tooltip' then this.extendLinkTooltip(options)
      when 'multi-cursor' then this.extendMultiCursor(options)
      when 'toolbar'      then this.extendToolbar(options)
    return super(name, options)

  extendAttribution: (options) ->

  extendLinkTooltip: (options) ->
    @editor.renderer.addStyles(
      '.editor-container a': { 'color': '#06c' }
      '.editor-container #link-tooltip':
        'border': '1px solid #ccc'
        'box-shadow': '0px 0px 5px #ddd'
        'color': '#222'
      '.editor-container #link-tooltip a':
        'color': '#06c'
      '.editor-container #link-tooltip .input':
        'border': '1px solid #ccc'
        'margin': '0px'
        'padding': '3px'
    )

  extendMultiCursor: (options) ->
    options.template =
     '

      '
    @editor.renderer.addStyles(
      '.editor-container .cursor-name':
        'border-radius': '3px'
        'font-size': '11px'
        'font-family': 'Arial'
        'margin-left': '-50%'
        'padding': '4px 10px'
      '.editor-container .cursor-triangle':
        'border-left': '4px solid transparent'
        'border-right': '4px solid transparent'
        'border-top': '4px solid transparent'
        'display': 'block'
        'height': '0px'
        'margin-bottom': '-1px'
        'margin-left': '-3px'
        'width': '0px'
      '.editor-container .cursor.top > .cursor-flag': { 'bottom': '100%', 'top': 'auto' }
    )

  extendToolbar: (options) ->
    this.addStyleSheet('styles/snow.css')
    options.container = document.getElementById(options.container) if _.isString(options.container)
    _.each(options.container.querySelectorAll('.font-name, .font-size'), (select) =>
      picker = new ScribePicker(select)
      @pickers.push(picker)
    )
    _.each(['fore-color', 'back-color'], (css) =>
      select = options.container.querySelector(".#{css}")
      return unless select?
      picker = new ScribeColorPicker(select)
      @pickers.push(picker)
      ScribeDOM.addClass(picker.container.querySelector('.picker-label'), 'format-button')
      _.each(picker.container.querySelectorAll('.picker-item'), (item, i) ->
        ScribeDOM.addClass(item, 'primary-color') if i < 7
      )
      format = @editor.doc.formatManager.formats[css]
      if format?
        format.styles = _.reduce(ScribeSnowTheme.COLORS, (colors, c) ->
          colors[c] = "rgb(#{parseInt(c.substr(1,2), 16)}, #{parseInt(c.substr(3,2), 16)}, #{parseInt(c.substr(5,2), 16)})"
          return colors
        , {})
        format.defaultStyle = if css == 'fore-color' then '#000000' else '#ffffff'
    )

module.exports = ScribeSnowTheme


_.each(_.extend(range.getFormats(), activeFormats), (value, key) =>

keep keep keep keep keep replace replace replace replace replace replace replace replace replace replace replace replace replace keep keep keep keep

ScribeDOM =
  ELEMENT_NODE: 1
  NOBREAK_SPACE:  " "
  TEXT_NODE: 3
  ZERO_WIDTH_NOBREAK_SPACE:  "\uFEFF"

  addClass: (node, cssClass) ->
    return if ScribeDOM.hasClass(node, cssClass)
    if node.classList?
      node.classList.add(cssClass)
    else if node.className?
      node.className += ' ' + cssClass

  addEventListener: (node, eventName, listener) ->
    names = eventName.split(' ')
    if names.length > 1
      return _.each(names, (name) ->
        ScribeDOM.addEventListener(node, name, listener)
      )
    callback = (event) ->
      event ?= ScribeDOM.getWindow(node).event
      event.target ?= event.srcElement
      event.which ?= event.keyCode
      bubbles = listener.call(null, event)
      if bubbles == false
        if event.preventDefault? and event.stopPropagation?
          event.preventDefault()
          event.stopPropagation()
        else
          event.returnValue = false
      return bubbles
    if node.addEventListener?
      node.addEventListener(eventName, callback)
    else if node.attachEvent?
      node.attachEvent("on#{eventName}", callback)
    else
      throw new Error("No add event support")

  getChildIndex: (node) ->
    index = 0
    while node.previousSibling?
      node = previousSibling
      index += 1
    return index

  getClasses: (node) ->
    if node.classList
      return _.clone(node.classList)
    else if node.className?
      return node.className.split(' ')

  getDefaultOption: (select) ->
    option = select.querySelector('option[selected]')
    if option?
      return option
    else
      # IE8
      for o,i in select.options
        if o.defaultSelected
          return o
    return null

  getText: (node) ->
    switch node.nodeType
      when ScribeDOM.ELEMENT_NODE
        return if node.tagName == "BR" then "" else node.textContent or node.innerText or ""
      when ScribeDOM.TEXT_NODE then return node.data or ""
      else return ""

  getWindow: (node) ->
    return node.ownerDocument.defaultView or node.ownerDocument.parentWindow

  hasClass: (node, cssClass) ->
    if node.classList?
      return node.classList.contains(cssClass)
    else if node.className?
      return _.indexOf(ScribeDOM.getClasses(node), cssClass) > -1
    return false

  mergeNodes: (node1, node2) ->
    return node2 if !node1?
    return node1 if !node2?
    this.moveChildren(node1, node2)
    node2.parentNode.removeChild(node2)
    if (node1.tagName == 'OL' || node1.tagName == 'UL') && node1.childNodes.length == 2
      ScribeDOM.mergeNodes(node1.firstChild, node1.lastChild)
    return node1

  moveChildren: (newParent, oldParent) ->
    _.each(_.clone(oldParent.childNodes), (child) ->
      newParent.appendChild(child)
    )

  normalize: (node) ->
    # Credit: Tim Down - http://stackoverflow.com/questions/2023255/node-normalize-crashes-in-ie6
    child = node.firstChild
    while (child)
      if (child.nodeType == 3)
        while ((nextChild = child.nextSibling) && nextChild.nodeType == 3)
          child.appendData(nextChild.data)
          node.removeChild(nextChild)
      child = child.nextSibling

  removeAttributes: (node, exception = []) ->
    exception = [exception] if _.isString(exception)
    _.each(_.clone(node.attributes), (attrNode, value) ->
      node.removeAttribute(attrNode.name) unless _.indexOf(exception, attrNode.name) > -1
    )

  removeClass: (node, cssClass) ->
    return unless ScribeDOM.hasClass(node, cssClass)
    if node.classList?
      return node.classList.remove(cssClass)
    else if node.className?
      classArray = ScribeDOM.getClasses(node)
      classArray.splice(_.indexOf(classArray, cssClass), 1)
      node.className = classArray.join(' ')

  removeNode: (node) ->
    node.parentNode?.removeChild(node)

  resetSelect: (select) ->
    option = ScribeDOM.getDefaultOption(select)
    if option?
      option.selected = true
      ScribeDOM.triggerEvent(select, 'change')
    else
      select.selectedIndex = null

  setText: (node, text) ->
    switch node.nodeType
      when ScribeDOM.ELEMENT_NODE
        if node.textContent?
          node.textContent = text
        else
          node.innerText = text
      when ScribeDOM.TEXT_NODE then node.data = text
      else return # Noop

  switchTag: (node, newTag) ->
    return if node.tagName == newTag
    newNode = node.ownerDocument.createElement(newTag)
    this.moveChildren(newNode, node)
    node.parentNode.replaceChild(newNode, node)
    newNode.className = node.className if node.className
    newNode.id = node.id if node.id
    return newNode

  toggleClass: (node, className, state) ->
    state = !ScribeDOM.hasClass(node, className) unless state?
    if state
      ScribeDOM.addClass(node, className)
    else
      ScribeDOM.removeClass(node, className)

  triggerEvent: (elem, eventName, bubble, cancels) ->
    if elem.ownerDocument.createEvent
      evt = elem.ownerDocument.createEvent("HTMLEvents")
      evt.initEvent(eventName, bubble, cancels)
      elem.dispatchEvent(evt)
    else
      elem.fireEvent("on#{eventName}", cancels)

  unwrap: (node) ->
    ret = node.firstChild
    next = node.nextSibling
    _.each(_.clone(node.childNodes), (child) ->
      node.parentNode.insertBefore(child, next)
    )
    node.parentNode.removeChild(node)
    return ret

  wrap: (wrapper, node) ->
    node.parentNode.insertBefore(wrapper, node)
    wrapper.appendChild(node)
    return wrapper

module.exports = ScribeDOM


require_relative '../lib/scribe_driver'

describe "Test Delete" do
  include ScribeDriver

  before do
    setup_test_suite
  end

  after do
    @driver.quit
  end

  def run_delete_test(initial, delete_delta, err_msg)
    reset_scribe initial
    apply_delta delete_delta, err_msg
  end

  it "should delete plain text" do
    initial = { "startLength" => 0,
                "endLength" => 4,
                "ops" => [{ "value" => "abc\n"}]
    }

    delta = { "startLength" => 4,
              "endLength" => 3,
              "ops" => [{ "start" => 0, "end" => 2, "attributes" => {}},
                        { "start" => 3, "end" => 4, "attributes" => {}}]
    }

    run_delete_test initial, delta, "Failed deleting plain text at index 3"
  end

  it "should delete formatted text" do
    initial = { "startLength" => 0,
              "endLength" => 4,
              "ops" => [{ "value" => "abc", "attributes" => {"fore-color" => "red"}},
                        { "value" => "\n" }]
    }

    delta = { "startLength" => 4,
              "endLength" => 3,
              "ops" => [{ "start" => 0, "end" => 2, "attributes" => {}},
                        { "start" => 3, "end" => 4, "attributes" => {}}]
    }

    run_delete_test initial, delta, "Failed deleting formatted text at index 3"
  end

  it "should delete newline and retain formatting" do
    initial = { "startLength" => 0,
              "endLength" => 8,
              "ops" => [{ "value" => "abc", "attributes" => {"fore-color" => "red"} },
                        { "value" => "\n", "attributes" => {} },
                        { "value" => "def", "attributes" => {"fore-color" => "red"} },
                        { "value" => "\n" }]
    }

    delta = { "startLength" => 8,
              "endLength" => 5,
              "ops" => [{ "start" => 0, "end" => 2, "attributes" => {} },
                        { "start" => 5, "end" => 8, "attributes" => {} }]
    }
    run_delete_test initial, delta, "Failed deleting formatted, newlined text."
  end

  it "should delete newline from empty document" do
    initial = { "startLength" => 0,
                "endLength" => 1,
                "ops" => [{ "value" => "\n"}]
    }

    reset_scribe initial
    @editor.send_keys :enter
    @editor.send_keys :backspace
    @editor.send_keys "a"
    expected = { "startLength" => 0,
                 "endLength" => 2,
                 "ops" => [{ "value" => "a\n", "attributes" => {} }]
    }
    assert ScribeDriver::JS.editor_delta_equals(expected),
      "Deleting a newline and then inserting into the empty document fails."

  end

  it "should pull formatting onto previous line when newline is deleted" do
    initial = { "startLength" => 0,
              "endLength" => 10,
              "ops" => [{ "value" => "ab", "attributes" => {} },
                        { "value" => "cd", "attributes" => { "fore-color" => "red" }},
                        { "value" => "\n", "attributes" => {} },
                        { "value" => "ef", "attributes" => {} },
                        { "value" => "gh", "attributes" => {"fore-color" => "red"}},
                        { "value" => "\n" }]
    }

    delta = { "startLength" => 10,
              "endLength" => 9,
              "ops" => [{ "start" => 0, "end" => 4, "attributes" => {} },
                        { "start" => 5, "end" => 10, "attributes" => {} }]
    }

    run_delete_test initial, delta, "Failed deleting newline that precedes formatting."
  end

  it "should delete plain and formatted text" do
    initial = { "startLength" => 0,
                "endLength" => 8,
                "ops" => [{ "value" => "abc", "attributes" => {} },
                          { "value" => "\n", "attributes" => {} },
                          { "value" => "def", "attributes" => {"fore-color" => "red"} },
                          { "value" => "\n" }]
    }

    delta = { "startLength" => 8,
              "endLength" => 5,
              "ops" => [{ "start" => 0, "end" => 2, "attributes" => {} },
                        { "start" => 5, "end" => 8, "attributes" => {} }]
    }
    run_delete_test initial, delta, "Failed deleting plain and formatted, newlined text."
  end
end


ScribeDOM = require('../dom')

class ScribePicker
  constructor: (@select) ->
    @container = @select.ownerDocument.createElement('div')
    _.each(ScribeDOM.getClasses(@select), (css) =>
      ScribeDOM.addClass(@container, css)
    )
    title = @select.getAttribute('title')
    @container.setAttribute('title', title) if title
    ScribeDOM.addClass(@container, 'picker')
    @label = @select.ownerDocument.createElement('div')
    ScribeDOM.addClass(@label, 'picker-label')
    @container.appendChild(@label)
    picker = this.buildPicker()
    @container.appendChild(picker)
    selected = picker.querySelector('.selected')
    ScribeDOM.setText(@label, ScribeDOM.getText(selected)) if selected?
    ScribeDOM.addEventListener(@label, 'click', =>
      # Defer to avoid document click handler that closes all dropdowns
      hasClass = ScribeDOM.hasClass(@container, 'expanded')
      _.defer( =>
        ScribeDOM.toggleClass(@container, 'expanded', !hasClass)
      )
    )
    ScribeDOM.addEventListener(@select.ownerDocument, 'click', =>
      ScribeDOM.removeClass(@container, 'expanded')
    )
    ScribeDOM.addEventListener(@select, 'change', =>
      option = @container.querySelectorAll('.picker-item')[@select.selectedIndex]
      this.selectItem(option)
      ScribeDOM.toggleClass(@label, 'active', option != selected)
    )
    @select.parentNode.insertBefore(@container, @select)

  buildItem: (picker, option, index) ->
    item = @select.ownerDocument.createElement('div')
    ScribeDOM.addClass(item, 'picker-item')
    ScribeDOM.setText(item, ScribeDOM.getText(option))
    ScribeDOM.addClass(item, 'selected') if @select.selectedIndex == index
    ScribeDOM.addEventListener(item, 'click', =>
      this.selectItem(item)
      @select.selectedIndex = index
      ScribeDOM.triggerEvent(@select, 'change', true, true)
      return true
    )
    return item

  buildPicker: ->
    picker = @select.ownerDocument.createElement('div')
    ScribeDOM.addClass(picker, 'picker-options')
    _.each(@select.querySelectorAll('option'), (option, i) =>
      item = this.buildItem(picker, option, i)
      picker.appendChild(item)
    )
    @select.style.display = 'none'
    return picker

  close: ->
    ScribeDOM.removeClass(@container, 'expanded')

  selectItem: (item) ->
    selected = @container.querySelector('.selected')
    ScribeDOM.removeClass(selected, 'selected') if selected?
    if item?
      ScribeDOM.addClass(item, 'selected')
      ScribeDOM.setText(@label, ScribeDOM.getText(item))
    else
      @label.innerHTML = ' '

module.exports = ScribePicker


ScribeDOM   = require('./dom')
ScribeUtils = require('./utils')

class ScribeLeafFormat
  constructor: (@root, @keyName) ->

  clean: (node) ->
    ScribeDOM.removeAttributes(node)
    return node

  createContainer: ->
    throw new Error("Descendants should implement")

  matchContainer: (container) ->
    throw new Error("Descendants should implement")

  preformat: (value) ->
    throw new Error("Descendants should implement")

class ScribeTagFormat extends ScribeLeafFormat
  constructor: (@root, @keyName, @tagName) ->
    super

  approximate: (value) ->
    throw new Error('Tag format must have truthy value') unless value
    return true

  clean: (node) ->
    node = super(node)
    node = ScribeDOM.switchTag(node, @tagName) unless node.tagName == @tagName
    return node

  createContainer: ->
    return @root.ownerDocument.createElement(@tagName)

  matchContainer: (container) ->
    return container.tagName == @tagName

  preformat: (value) ->
    @root.ownerDocument.execCommand(@keyName, false, value)

class ScribeSpanFormat extends ScribeTagFormat
  constructor: (@root, @keyName) ->
    super(@root, @keyName, 'SPAN')

  clean: (node) ->
    # cannot super because LeafFormat removes all attributes
    node = ScribeDOM.switchTag(node, @tagName) unless node.tagName == @tagName
    return node

  approximate: (value) ->
    throw new Error("Descendants should implement")

class ScribeClassFormat extends ScribeSpanFormat
  constructor: (@root, @keyName) ->
    super

  approximate: (value) ->
    parts = value.split('-')
    if parts.length > 1 and parts[0] == @keyName
      return parts.slice(1).join('-')
    return false

  clean: (node) ->
    ScribeDOM.removeAttributes(node, 'class')
    return node

  createContainer: (value) ->
    container = super(value)
    ScribeDOM.addClass(container, "#{@keyName}-#{value}")
    return container

  matchContainer: (container) ->
    if super(container)
      classList = ScribeDOM.getClasses(container)
      for css in classList
        value = this.approximate(css)
        return value if value
    return false

class ScribeStyleFormat extends ScribeSpanFormat
  @getStyleObject: (container) ->
    # Iterating through container.style upsets IE8
    styleString = container.getAttribute('style') or ''
    obj = _.reduce(styleString.split(';'), (styles, str) ->
      [name, value] = str.split(':')
      if name and value
        name = _.str.trim(name)
        value = _.str.trim(value)
        styles[name.toLowerCase()] = value
      return styles
    , {})
    return obj

  constructor: (@root, @keyName, @cssName, @defaultStyle, @styles) ->
    super

  approximate: (cssValue) ->
    for key,value of @styles
      if value.toUpperCase() == cssValue.toUpperCase()
        return if key == @defaultStyle then false else key
    return false

  clean: (node) ->
    node = super(node)
    ScribeDOM.removeAttributes(node, 'style')
    styleObj = ScribeStyleFormat.getStyleObject(node)
    node.removeAttribute('style')
    if styleObj[@cssName]
      style = this.approximate(styleObj[@cssName])
      node.setAttribute('style', "#{@cssName}: #{@styles[style]};") if style    # PhantomJS adds a trailing space if we use node.style
    return node

  createContainer: (value) ->
    container = super(value)
    cssName = _.str.camelize(@cssName)
    style = this.approximate(value)
    container.setAttribute('style', "#{@cssName}: #{@styles[style]};") if style # PhantomJS adds a trailing space if we use node.style
    return container

  matchContainer: (container) ->
    style = container.style?[_.str.camelize(@cssName)]
    return if style then this.approximate(style) else false

  preformat: (value) ->
    value = this.approximate(value) or @defaultStyle
    @root.ownerDocument.execCommand(_.str.camelize(@keyName), false, @styles[value])

class ScribeBoldFormat extends ScribeTagFormat
  constructor: (@root) ->
    super(@root, 'bold', 'B')

  matchContainer: (container) ->
    return super(container) or container.style?.fontWeight == 'bold'

class ScribeItalicFormat extends ScribeTagFormat
  constructor: (@root) ->
    super(@root, 'italic', 'I')

  matchContainer: (container) ->
    return super(container) or container.style?.fontStyle == 'italic'

class ScribeStrikeFormat extends ScribeTagFormat
  constructor: (@root) ->
    super(@root, 'strike', 'S')

  matchContainer: (container) ->
    return super(container) or container.style?.textDecoration == 'line-through'

  preformat: (value) ->
    @root.ownerDocument.execCommand('strikeThrough', false, value)

class ScribeUnderlineFormat extends ScribeTagFormat
  constructor: (@root) ->
    super(@root, 'underline', 'U')

  matchContainer: (container) ->
    return super(container) or container.style?.textDecoration == 'underline'

class ScribeLinkFormat extends ScribeTagFormat
  constructor: (@root) ->
    super(@root, 'link', 'A')

  approximate: (value) ->
    value = 'http://' + value unless value.match(/^https?:\/\//)
    return value

  clean: (node) ->
    ScribeDOM.removeAttributes(node, ['href', 'title'])
    return node

  createContainer: (value) ->
    link = super(value)
    link.href = this.approximate(value)
    link.title = link.href
    return link

  matchContainer: (container) ->
    return if super(container) then container.getAttribute('href') else false

class ScribeColorFormat extends ScribeStyleFormat
  @COLORS:
    'black'   : 'rgb(0, 0, 0)'
    'red'     : 'rgb(255, 0, 0)'
    'blue'    : 'rgb(0, 0, 255)'
    'lime'    : 'rgb(0, 255, 0)'
    'teal'    : 'rgb(0, 255, 255)'
    'magenta' : 'rgb(255, 0, 255)'
    'yellow'  : 'rgb(255, 255, 0)'
    'white'   : 'rgb(255, 255, 255)'

  @normalizeColor: (value) ->
    value = value.replace(/\ /g, '')
    if value[0] == '#' and value.length == 4
      return _.map(value.slice(1), (letter) ->
        parseInt(letter + letter, 16)
      )
    else if value[0] == '#' and value.length == 7
      return [
        parseInt(value.slice(1,3), 16)
        parseInt(value.slice(3,5), 16)
        parseInt(value.slice(5,7), 16)
      ]
    else if value.indexOf('rgb') == 0
      colors = value.slice(value.indexOf('(') + 1, value.indexOf(')')).split(',')
      return _.map(colors, (color) ->
        parseInt(color)
      )
    else
      return [0,0,0]

  constructor: (@root, @keyName, @cssName, @defaultStyle, @styles) ->
    super

  approximate: (value) ->
    return false unless value
    return value if @styles[value]?
    color = ScribeUtils.findClosestPoint(value, @styles, ScribeColorFormat.normalizeColor)
    return if color == @defaultStyle then false else color

class ScribeBackColorFormat extends ScribeColorFormat
  constructor: (@root) ->
    super(@root, 'back-color', 'background-color', 'white', ScribeColorFormat.COLORS)

class ScribeFontNameFormat extends ScribeStyleFormat
  @normalizeFont: (fontStr) ->
    return _.map(fontStr.toUpperCase().split(','), (font) ->
      return _.str.trim(font, "'\" ")
    )

  constructor: (@root) ->
    super(@root, 'font-name', 'font-family', 'sans-serif', {
      'sans-serif': "'Helvetica', 'Arial', sans-serif"
      'serif'     : "'Times New Roman', serif"
      'monospace' : "'Courier New', monospace"
    })

  approximate: (value) ->
    values = ScribeFontNameFormat.normalizeFont(value)
    for key,fonts of @styles
      fonts = ScribeFontNameFormat.normalizeFont(fonts)
      return key if _.intersection(fonts, values).length > 0
    return false

class ScribeFontSizeFormat extends ScribeStyleFormat
  @SCALE: 6.75      # Conversion from execCommand size to px

  constructor: (@root) ->
    super(@root, 'font-size', 'font-size', 'normal', {
      'huge'  : '32px'
      'large' : '18px'
      'normal': '13px'
      'small' : '10px'
    })

  approximate: (value) ->
    return value if @styles[value]?
    if _.isString(value) and value.indexOf('px') > -1
      value = parseInt(value)
    else
      value = parseInt(value) * ScribeFontSizeFormat.SCALE
    size = ScribeUtils.findClosestPoint(value, @styles, parseInt)
    return if size == @defaultStyle then false else size

  preformat: (value) ->
    value = this.approximate(value) or @defaultStyle
    size = Math.round(parseInt(@styles[value]) / ScribeFontSizeFormat.SCALE)
    @root.ownerDocument.execCommand(_.str.camelize(@keyName), false, size)

class ScribeForeColorFormat extends ScribeColorFormat
  constructor: (@root) ->
    super(@root, 'fore-color', 'color', 'black', ScribeColorFormat.COLORS)

module.exports =
  Leaf  : ScribeLeafFormat
  Tag   : ScribeTagFormat
  Span  : ScribeSpanFormat
  Class : ScribeClassFormat
  Style : ScribeStyleFormat

  Bold      : ScribeBoldFormat
  Italic    : ScribeItalicFormat
  Link      : ScribeLinkFormat
  Strike    : ScribeStrikeFormat
  Underline : ScribeUnderlineFormat

  BackColor : ScribeBackColorFormat
  FontName  : ScribeFontNameFormat
  FontSize  : ScribeFontSizeFormat
  ForeColor : ScribeForeColorFormat


CachedCollectionManager.onLogin(async () => {

const { roles } = await APIClient.v1.get('roles.list');

keep keep keep keep replace keep keep keep keep

// if a role is checked before this collection is populated, it will return undefined

await Roles.upsert({ _id: role._id }, role);

import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';

import { getUserPreference } from '../../utils/client';
import { mainReady, Layout, iframeLogin } from '../../ui-utils';
import { settings } from '../../settings';
import { CachedChatSubscription, Roles, Users } from '../../models';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { tooltip } from '../../ui/client/components/tooltip';
import { callbacks } from '../../callbacks/client';
import { isSyncReady } from '../../../client/lib/userData';

import './main.html';

function executeCustomScript(script) {
	eval(script);//eslint-disable-line
}

function customScriptsOnLogout() {
	const script = settings.get('Custom_Script_On_Logout') || '';
	if (script.trim()) {
		executeCustomScript(script);
	}
}

callbacks.add('afterLogoutCleanUp', () => customScriptsOnLogout(), callbacks.priority.LOW, 'custom-script-on-logout');

Template.main.helpers({
	removeSidenav: () => Layout.isEmbedded() && !/^\/admin/.test(FlowRouter.current().route.path),
	logged: () => {
		if (!!Meteor.userId() || (settings.get('Accounts_AllowAnonymousRead') === true && Session.get('forceLogin') !== true)) {
			document.documentElement.classList.add('noscroll');
			document.documentElement.classList.remove('scroll');
			return true;
		}

		document.documentElement.classList.add('scroll');
		document.documentElement.classList.remove('noscroll');
		return false;
	},
	useIframe: () => {
		const iframeEnabled = typeof iframeLogin !== 'undefined';
		return iframeEnabled && iframeLogin.reactiveEnabled.get();
	},
	iframeUrl: () => {
		const iframeEnabled = typeof iframeLogin !== 'undefined';
		return iframeEnabled && iframeLogin.reactiveIframeUrl.get();
	},
	subsReady: () => {
		const subscriptionsReady = CachedChatSubscription.ready.get();
		const settingsReady = settings.cachedCollection.ready.get();
		const ready = !Meteor.userId() || (isSyncReady.get() && subscriptionsReady && settingsReady);

		CachedCollectionManager.syncEnabled = ready;
		mainReady.set(ready);

		return ready;
	},
	hasUsername: () => {
		const uid = Meteor.userId();
		const user = uid && Users.findOne({ _id: uid }, { fields: { username: 1 } });
		return (user && user.username) || (!uid && settings.get('Accounts_AllowAnonymousRead'));
	},
	requirePasswordChange: () => {
		const user = Meteor.user();
		return user && user.requirePasswordChange === true;
	},
	require2faSetup: () => {
		const user = Meteor.user();

		// User is already using 2fa
		if (!user || (user.services.totp !== undefined && user.services.totp.enabled) || (user.services.email2fa !== undefined && user.services.email2fa.enabled)) {
			return false;
		}

		const mandatoryRole = Roles.findOne({ _id: { $in: user.roles }, mandatory2fa: true });
		return mandatoryRole !== undefined;
	},
	CustomScriptLoggedOut: () => {
		const script = settings.get('Custom_Script_Logged_Out') || '';
		if (script.trim()) {
			executeCustomScript(script);
		}
	},
	CustomScriptLoggedIn: () => {
		const script = settings.get('Custom_Script_Logged_In') || '';
		if (script.trim()) {
			executeCustomScript(script);
		}
	},
	embeddedVersion: () => {
		if (Layout.isEmbedded()) {
			return 'embedded-view';
		}
	},
	readReceiptsEnabled: () => {
		if (settings.get('Message_Read_Receipt_Store_Users')) {
			return 'read-receipts-enabled';
		}
	},
});

Template.main.onCreated(function() {
	tooltip.init();
});

Template.main.onRendered(function() {
	Tracker.autorun(function() {
		const userId = Meteor.userId();

		if (getUserPreference(userId, 'hideUsernames')) {
			$(document.body).on('mouseenter', 'button.thumb', (e) => {
				const avatarElem = $(e.currentTarget);
				const username = avatarElem.attr('data-username');
				if (username) {
					e.stopPropagation();
					tooltip.showElement($('<span>').text(username), avatarElem);
				}
			});

			$(document.body).on('mouseleave', 'button.thumb', () => {
				tooltip.hide();
			});

			return;
		}

		$(document.body).off('mouseenter', 'button.thumb');
		$(document.body).off('mouseleave', 'button.thumb');
	});
});


import { Box, Margins } from '@rocket.chat/fuselage';
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import React from 'react';

import Logo from '../../components/Logo';
import ScrollableContentWrapper from '../../components/ScrollableContentWrapper';
import { useTranslation } from '../../contexts/TranslationContext';
import './SideBar.css';

function SideBar({ logoSrc = 'images/logo/logo.svg', currentStep = 1, steps = [] }) {
	const t = useTranslation();
	const small = useMediaQuery('(max-width: 760px)');

	return (
		<Box
			is='aside'
			className='SetupWizard__SideBar'
			flexGrow={0}
			flexShrink={1}
			flexBasis={small ? 'auto' : '350px'}
			maxHeight='sh'
			display='flex'
			flexDirection='column'
			flexWrap='nowrap'
			style={{ overflow: 'hidden' }}
		>
			<Box
				is='header'
				marginBlockStart={small ? 'x16' : 'x32'}
				marginBlockEnd={small ? 'none' : 'x32'}
				marginInline='x24'
				display='flex'
				flexDirection='row'
				flexWrap='wrap'
				alignItems='center'
			>
				<Logo src={logoSrc} width='auto' height='x24' margin='x4' />
				<Box
					is='span'
					margin='x4'
					paddingBlock='x4'
					paddingInline='x8'
					color='alternative'
					fontScale='micro'
					style={{
						whiteSpace: 'nowrap',
						textTransform: 'uppercase',
						backgroundColor: 'var(--color-dark, #2f343d)',
						borderRadius: '9999px',
					}}
				>
					{t('Setup_Wizard')}
				</Box>
			</Box>

			{!small && (
				<ScrollableContentWrapper>
					<Box flexGrow={1} marginBlockEnd='x16' paddingInline='x32'>
						<Margins blockEnd='x16'>
							<Box is='h2' fontScale='h1' color='default'>
								{t('Setup_Wizard')}
							</Box>
							<Box is='p' color='hint' fontScale='p1'>
								{t('Setup_Wizard_Info')}
							</Box>
						</Margins>

						<Box is='ol'>
							{steps.map(({ step, title }) => (
								<Box
									key={step}
									is='li'
									className={[
										'SetupWizard__SideBar-step',
										step < currentStep && 'SetupWizard__SideBar-step--past',
									]
										.filter(Boolean)
										.join(' ')}
									data-number={step}
									marginBlock='x32'
									marginInline='neg-x8'
									display='flex'
									alignItems='center'
									fontScale='p2'
									color={
										(step === currentStep && 'primary') ||
										(step < currentStep && 'default') ||
										'disabled'
									}
									style={{ position: 'relative' }}
								>
									{title}
								</Box>
							))}
						</Box>
					</Box>
				</ScrollableContentWrapper>
			)}
		</Box>
	);
}

export default SideBar;


import { Migrations } from '../../../app/migrations';
import { Analytics } from '../../../app/models/server';

Migrations.add({
	version: 182,
	up() {
		Analytics.remove({});
	},
});


import { Box, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React from 'react';

import Card from '../../../components/Card';
import { useSetModal } from '../../../contexts/ModalContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import InstancesModal from './InstancesModal';
import UsagePieGraph from './UsagePieGraph';

const InstancesCard = ({ instances }) => {
	const t = useTranslation();

	const setModal = useSetModal();

	const handleModal = useMutableCallback(() => {
		setModal(<InstancesModal instances={instances} onClose={() => setModal()} />);
	});

	return (
		<Card alignSelf='flex-start'>
			<Card.Title>{t('Instances')}</Card.Title>
			<Card.Body>
				<Card.Col>
					<Card.Col.Section>
						<Box display='flex' flexDirection='row' justifyContent='center'>
							<UsagePieGraph label={t('Instances_Health')} used={300} total={300} size={180} />
						</Box>
					</Card.Col.Section>
				</Card.Col>
			</Card.Body>
			<Card.Footer>
				<ButtonGroup align='end'>
					<Button small onClick={handleModal}>
						{t('Details')}
					</Button>
				</ButtonGroup>
			</Card.Footer>
		</Card>
	);
};

export default InstancesCard;


if (!user || (user.services.totp !== undefined && user.services.totp.enabled) || (user.services.email2fa !== undefined && user.services.email2fa.enabled)) {

const mandatoryRole = Roles.findOne({ _id: { $in: user.roles }, mandatory2fa: true });

const is2faEnabled = settings.get('Accounts_TwoFactorAuthentication_Enabled');


The MIT License (MIT)

Copyright (c) 2017 Karl STEIN

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

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

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


import React, { memo } from 'react';

import { useTranslation } from '../../contexts/TranslationContext';
import Sidebar from './Sidebar';

const ItemsAssembler = ({ items, currentPath }) => {
	const t = useTranslation();
	return items.map(
		({ href, pathSection, i18nLabel, name, icon, permissionGranted, pathGroup, tag }) => (
			<Sidebar.NavigationItem
				permissionGranted={permissionGranted}
				pathGroup={pathGroup}
				pathSection={href || pathSection}
				icon={icon}
				label={t(i18nLabel || name)}
				key={i18nLabel || name}
				currentPath={currentPath}
				tag={t(tag)}
			/>
		),
	);
};

export default memo(ItemsAssembler);


import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';

import { CustomUserStatus } from '../../../models';
import { API } from '../api';
import { findCustomUserStatus } from '../lib/custom-user-status';

API.v1.addRoute('custom-user-status.list', { authRequired: true }, {
	get() {
		const { offset, count } = this.getPaginationItems();
		const { sort, query } = this.parseJsonQuery();

		return API.v1.success(Promise.await(findCustomUserStatus({
			query,
			pagination: {
				offset,
				count,
				sort,
			},
		})));
	},
});

API.v1.addRoute('custom-user-status.create', { authRequired: true }, {
	post() {
		check(this.bodyParams, {
			name: String,
			statusType: Match.Maybe(String),
		});

		const userStatusData = {
			name: this.bodyParams.name,
			statusType: this.bodyParams.statusType,
		};

		Meteor.runAsUser(this.userId, () => {
			Meteor.call('insertOrUpdateUserStatus', userStatusData);
		});

		return API.v1.success({
			customUserStatus: CustomUserStatus.findOneByName(userStatusData.name),
		});
	},
});

API.v1.addRoute('custom-user-status.delete', { authRequired: true }, {
	post() {
		const { customUserStatusId } = this.bodyParams;
		if (!customUserStatusId) {
			return API.v1.failure('The "customUserStatusId" params is required!');
		}

		Meteor.runAsUser(this.userId, () => Meteor.call('deleteCustomUserStatus', customUserStatusId));

		return API.v1.success();
	},
});

API.v1.addRoute('custom-user-status.update', { authRequired: true }, {
	post() {
		check(this.bodyParams, {
			_id: String,
			name: String,
			statusType: Match.Maybe(String),
		});

		const userStatusData = {
			_id: this.bodyParams._id,
			name: this.bodyParams.name,
			statusType: this.bodyParams.statusType,
		};

		const customUserStatus = CustomUserStatus.findOneById(userStatusData._id);

		// Ensure the message exists
		if (!customUserStatus) {
			return API.v1.failure(`No custom user status found with the id of "${ userStatusData._id }".`);
		}

		Meteor.runAsUser(this.userId, () => {
			Meteor.call('insertOrUpdateUserStatus', userStatusData);
		});

		return API.v1.success({
			customUserStatus: CustomUserStatus.findOneById(userStatusData._id),
		});
	},
});


const script = settings.get('Custom_Script_Logged_Out') || '';

return mandatoryRole !== undefined && is2faEnabled;

import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';

import { hasAtLeastOnePermission } from './hasPermission';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { APIClient } from '../../utils/client';
import { Roles } from '../../models/client';
import { rolesStreamer } from './lib/streamer';
import { registerAdminSidebarItem } from '../../../client/views/admin';

Meteor.startup(() => {
	CachedCollectionManager.onLogin(async () => {
		const { roles } = await APIClient.v1.get('roles.list');
		roles.forEach((role) => Roles.insert(role));
	});

	registerAdminSidebarItem({
		href: 'admin-permissions',
		i18nLabel: 'Permissions',
		icon: 'lock',
		permissionGranted() {
			return hasAtLeastOnePermission(['access-permissions', 'access-setting-permissions']);
		},
	});
	const events = {
		changed: (role) => {
			delete role.type;
			Roles.upsert({ _id: role.name }, role);
		},
		removed: (role) => {
			Roles.remove({ _id: role.name });
		},
	};

	Tracker.autorun((c) => {
		if (!Meteor.userId()) {
			return;
		}
		rolesStreamer.on('roles', (role) => events[role.type](role));
		c.stop();
	});
});


import { addAction, QuickActionsEnum } from '.';

addAction(QuickActionsEnum.MoveQueue, {
	groups: ['live'],
	id: QuickActionsEnum.MoveQueue,
	title: 'Move_queue',
	icon: 'burger-arrow-left',
	order: 1,
});

addAction(QuickActionsEnum.ChatForward, {
	groups: ['live'],
	id: QuickActionsEnum.ChatForward,
	title: 'Forward_chat',
	icon: 'balloon-arrow-top-right',
	order: 2,
});

addAction(QuickActionsEnum.Transcript, {
	groups: ['live'],
	id: QuickActionsEnum.Transcript,
	title: 'Transcript',
	icon: 'mail-arrow-top-right',
	order: 3,
});

addAction(QuickActionsEnum.CloseChat, {
	groups: ['live'],
	id: QuickActionsEnum.CloseChat,
	title: 'Close',
	icon: 'balloon-close-top-right',
	order: 5,
	color: 'danger',
});