if (!RedactorPlugins) var RedactorPlugins = {};

(function ($R) {
  $R.add('plugin', 'definedlinks', {
    init: function (app) {
      this.app = app;
      this.opts = app.opts;
      this.component = app.component;
      // local
      this.links = [];
    },
    // messages
    onmodal: {
      link: {
        open: function ($modal, $form) {
          if (!this.opts.definedlinks) return;
          this.$modal = $modal;
          this.$form = $form;
          this._load();
        }
      }
    },
    // private
    _load: function () {
      if (typeof this.opts.definedlinks === 'object') {
        this._build(this.opts.definedlinks);
      } else {
        $R.ajax.get({
          url: this.opts.definedlinks,
          success: this._build.bind(this)
        });
      }
    },
    _build: function (data) {
      var $selector = this.$modal.find('#redactor-defined-links');
      if ($selector.length === 0) {
        var $body = this.$modal.getBody();
        var $item = $R.dom('<div class="form-item" />');
        var $selector = $R.dom('<select id="redactor-defined-links" />');
        $item.append($selector);
        $body.prepend($item);
      }
      this.links = [];
      $selector.html('');
      $selector.off('change');
      for (var key in data) {
        if (!data.hasOwnProperty(key) || typeof data[key] !== 'object') {
          continue;
        }
        this.links[key] = data[key];
        var $option = $R.dom('<option>');
        $option.val(key);
        $option.html(data[key].name);
        $selector.append($option);
      }
      $selector.on('change', this._select.bind(this));
    },
    _select: function (e) {
      var formData = this.$form.getData();
      var key = $R.dom(e.target)
        .val();
      var data = {
        text: '',
        url: ''
      };
      if (key !== '0') {
        data.text = this.links[key].name;
        data.url = this.links[key].url;
      }
      if (formData.text !== '') {
        data = {
          url: data.url
        };
      }
      this.$form.setData(data);
    }
  });

  $R.add('plugin', 'fontcolor', {
    translations: {
      en: {
        "fontcolor": "Text Color",
        "text": "Text",
        "highlight": "Highlight"
      }
    },
    init: function (app) {
      this.app = app;
      this.opts = app.opts;
      this.lang = app.lang;
      this.inline = app.inline;
      this.toolbar = app.toolbar;
      this.selection = app.selection;
      // local
      this.colors = (this.opts.fontcolors) ? this.opts.fontcolors : ['#ffffff', '#000000', '#eeece1', '#1f497d', '#4f81bd', '#c0504d', '#9bbb59', '#8064a2', '#4bacc6', '#f79646', '#ffff00', '#f2f2f2', '#7f7f7f', '#ddd9c3', '#c6d9f0', '#dbe5f1', '#f2dcdb', '#ebf1dd', '#e5e0ec', '#dbeef3', '#fdeada', '#fff2ca', '#d8d8d8', '#595959', '#c4bd97', '#8db3e2', '#b8cce4', '#e5b9b7', '#d7e3bc', '#ccc1d9', '#b7dde8', '#fbd5b5', '#ffe694', '#bfbfbf', '#3f3f3f', '#938953', '#548dd4', '#95b3d7', '#d99694', '#c3d69b', '#b2a2c7', '#b7dde8', '#fac08f', '#f2c314', '#a5a5a5', '#262626', '#494429', '#17365d', '#366092', '#953734', '#76923c', '#5f497a', '#92cddc', '#e36c09', '#c09100', '#7f7f7f', '#0c0c0c', '#1d1b10', '#0f243e', '#244061', '#632423', '#4f6128', '#3f3151', '#31859b', '#974806', '#7f6000'];
    },
    // messages
    onfontcolor: {
      set: function (rule, value) {
        this._set(rule, value);
      },
      remove: function (rule) {
        this._remove(rule);
      }
    },
    // public
    start: function () {
      var btnObj = {
        title: this.lang.get('fontcolor')
      };
      var $dropdown = this._buildDropdown();
      this.$button = this.toolbar.addButtonAuto('fontcolor', btnObj);
      this.$button.setIcon('<i class="re-icon-fontcolor"></i>');
      this.$button.setDropdown($dropdown);
    },
    // private
    _buildDropdown: function () {
      var $dropdown = $R.dom('<div class="redactor-dropdown-cells">');
      this.$selector = this._buildSelector();
      this.$selectorText = this._buildSelectorItem('text', this.lang.get('text'));
      this.$selectorText.addClass('active');
      this.$selectorBack = this._buildSelectorItem('back', this.lang.get('highlight'));
      this.$selector.append(this.$selectorText);
      this.$selector.append(this.$selectorBack);
      this.$pickerText = this._buildPicker('textcolor');
      this.$pickerBack = this._buildPicker('backcolor');
      $dropdown.append(this.$selector);
      $dropdown.append(this.$pickerText);
      $dropdown.append(this.$pickerBack);
      this._buildSelectorEvents();
      $dropdown.width(242);
      return $dropdown;
    },
    _buildSelector: function () {
      var $selector = $R.dom('<div>');
      $selector.addClass('redactor-dropdown-selector');
      return $selector;
    },
    _buildSelectorItem: function (name, title) {
      var $item = $R.dom('<span>');
      $item.attr('rel', name)
        .html(title);
      $item.addClass('redactor-dropdown-not-close');
      return $item;
    },
    _buildSelectorEvents: function () {
      this.$selectorText.on('mousedown', function (e) {
        e.preventDefault();
        this.$selector.find('span')
          .removeClass('active');
        this.$pickerBack.hide();
        this.$pickerText.show();
        this.$selectorText.addClass('active');
      }.bind(this));
      this.$selectorBack.on('mousedown', function (e) {
        e.preventDefault();
        this.$selector.find('span')
          .removeClass('active');
        this.$pickerText.hide();
        this.$pickerBack.show();
        this.$selectorBack.addClass('active');
      }.bind(this));
    },
    _buildPicker: function (name) {
      var $box = $R.dom('<div class="re-dropdown-box-' + name + '">');
      var rule = (name == 'backcolor') ? 'background-color' : 'color';
      var len = this.colors.length;
      var self = this;
      var func = function (e) {
        e.preventDefault();
        var $el = $R.dom(e.target);
        self._set($el.data('rule'), $el.attr('rel'));
      };
      for (var z = 0; z < len; z++) {
        var color = this.colors[z];
        var $swatch = $R.dom('<span>');
        $swatch.attr({
          'rel': color,
          'data-rule': rule
        });
        $swatch.css({
          'background-color': color,
          'font-size': 0,
          'border': '2px solid #fff',
          'width': '22px',
          'height': '22px'
        });
        $swatch.on('mousedown', func);
        $box.append($swatch);
      }
      var $el = $R.dom('<a>');
      $el.attr({
        'href': '#'
      });
      $el.css({
        'display': 'block',
        'clear': 'both',
        'padding': '8px 5px',
        'font-size': '12px',
        'line-height': 1
      });
      $el.html(this.lang.get('none'));
      $el.on('click', function (e) {
        e.preventDefault();
        self._remove(rule);
      });
      $box.append($el);
      if (name == 'backcolor') $box.hide();
      return $box;
    },
    _set: function (rule, value) {
      var style = {};
      style[rule] = value;
      var args = {
        tag: 'span',
        style: style,
        type: 'toggle'
      };
      this.inline.format(args);
    },
    _remove: function (rule) {
      this.inline.remove({
        style: rule
      });
    }
  });

  $R.add('plugin', 'fontfamily', {
    translations: {
      en: {
        "fontfamily": "Font",
        "remove-font-family": "Remove Font Family"
      }
    },
    init: function (app) {
      this.app = app;
      this.opts = app.opts;
      this.lang = app.lang;
      this.inline = app.inline;
      this.toolbar = app.toolbar;
      // local
      this.fonts = (this.opts.fontfamily) ? this.opts.fontfamily : ['Arial', 'Helvetica', 'Georgia', 'Times New Roman', 'Monospace'];
    },
    // public
    start: function () {
      var dropdown = {};
      for (var i = 0; i < this.fonts.length; i++) {
        var font = this.fonts[i].replace(/'/g, '');
        dropdown[i] = {
          title: $R.dom('<span>').css('font-family', font).text(font).get().outerHTML,
          api: 'plugin.fontfamily.set',
          args: font,
        };
      }
      dropdown.remove = {
        title: this.lang.get('remove-font-family'),
        api: 'plugin.fontfamily.remove'
      };
      var $button = this.toolbar.addButtonAuto('fontfamily', {
        title: this.lang.get('fontfamily')
      });
      $button.setIcon('<i class="re-icon-fontfamily"></i>');
      $button.setDropdown(dropdown);
    },
    set: function (value) {
      var args = {
        tag: 'span',
        style: {
          'font-family': value
        },
        type: 'toggle'
      };
      this.inline.format(args);
    },
    remove: function () {
      this.inline.remove({
        style: 'font-family'
      });
    }
  });

  $R.add('plugin', 'table', {
    translations: {
      en: {
        "table": "Table",
        "insert-table": "Insert table",
        "insert-row-above": "Insert row above",
        "insert-row-below": "Insert row below",
        "insert-column-left": "Insert column left",
        "insert-column-right": "Insert column right",
        "add-head": "Add head",
        "delete-head": "Delete head",
        "delete-column": "Delete column",
        "delete-row": "Delete row",
        "delete-table": "Delete table"
      }
    },
    init: function (app) {
      this.app = app;
      this.lang = app.lang;
      this.opts = app.opts;
      this.caret = app.caret;
      this.editor = app.editor;
      this.toolbar = app.toolbar;
      this.component = app.component;
      this.inspector = app.inspector;
      this.insertion = app.insertion;
      this.selection = app.selection;
    },
    // messages
    ondropdown: {
      table: {
        observe: function (dropdown) {
          this._observeDropdown(dropdown);
        }
      }
    },
    onbottomclick: function () {
      this.insertion.insertToEnd(this.editor.getLastNode(), 'table');
    },
    // public
    start: function () {
      var dropdown = {
        observe: 'table',
        'insert-table': {
          title: this.lang.get('insert-table'),
          api: 'plugin.table.insert'
        },
        'insert-row-above': {
          title: this.lang.get('insert-row-above'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.addRowAbove'
        },
        'insert-row-below': {
          title: this.lang.get('insert-row-below'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.addRowBelow'
        },
        'insert-column-left': {
          title: this.lang.get('insert-column-left'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.addColumnLeft'
        },
        'insert-column-right': {
          title: this.lang.get('insert-column-right'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.addColumnRight'
        },
        'add-head': {
          title: this.lang.get('add-head'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.addHead'
        },
        'delete-head': {
          title: this.lang.get('delete-head'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.deleteHead'
        },
        'delete-column': {
          title: this.lang.get('delete-column'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.deleteColumn'
        },
        'delete-row': {
          title: this.lang.get('delete-row'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.deleteRow'
        },
        'delete-table': {
          title: this.lang.get('delete-table'),
          classname: 'redactor-table-item-observable',
          api: 'plugin.table.deleteTable'
        }
      };
      var obj = {
        title: this.lang.get('table')
      };
      var $button = this.toolbar.addButtonBefore('link', 'table', obj);
      $button.setIcon('<i class="re-icon-table"></i>');
      $button.setDropdown(dropdown);
    },
    insert: function () {
      var rows = 2;
      var columns = 3;
      var $component = this.component.create('table');
      for (var i = 0; i < rows; i++) {
        $component.addRow(columns);
      }
      $component = this.insertion.insertHtml($component);
      this.caret.setStart($component);
    },
    addRowAbove: function () {
      var $component = this._getComponent();
      if ($component) {
        var current = this.selection.getCurrent();
        var $row = $component.addRowTo(current, 'before');
        this.caret.setStart($row);
      }
    },
    addRowBelow: function () {
      var $component = this._getComponent();
      if ($component) {
        var current = this.selection.getCurrent();
        var $row = $component.addRowTo(current, 'after');
        this.caret.setStart($row);
      }
    },
    addColumnLeft: function () {
      var $component = this._getComponent();
      if ($component) {
        var current = this.selection.getCurrent();
        this.selection.save();
        $component.addColumnTo(current, 'left');
        this.selection.restore();
      }
    },
    addColumnRight: function () {
      var $component = this._getComponent();
      if ($component) {
        var current = this.selection.getCurrent();
        this.selection.save();
        $component.addColumnTo(current, 'right');
        this.selection.restore();
      }
    },
    addHead: function () {
      var $component = this._getComponent();
      if ($component) {
        this.selection.save();
        $component.addHead();
        this.selection.restore();
      }
    },
    deleteHead: function () {
      var $component = this._getComponent();
      if ($component) {
        var current = this.selection.getCurrent();
        var $head = $R.dom(current)
          .closest('thead');
        if ($head.length !== 0) {
          $component.removeHead();
          this.caret.setStart($component);
        } else {
          this.selection.save();
          $component.removeHead();
          this.selection.restore();
        }
      }
    },
    deleteColumn: function () {
      var $component = this._getComponent();
      if ($component) {
        var current = this.selection.getCurrent();
        var $currentCell = $R.dom(current)
          .closest('td, th');
        var nextCell = $currentCell.nextElement()
          .get();
        var prevCell = $currentCell.prevElement()
          .get();
        $component.removeColumn(current);
        if (nextCell) this.caret.setStart(nextCell);
        else if (prevCell) this.caret.setEnd(prevCell);
        else this.deleteTable();
      }
    },
    deleteRow: function () {
      var $component = this._getComponent();
      if ($component) {
        var current = this.selection.getCurrent();
        var $currentRow = $R.dom(current)
          .closest('tr');
        var nextRow = $currentRow.nextElement()
          .get();
        var prevRow = $currentRow.prevElement()
          .get();
        $component.removeRow(current);
        if (nextRow) this.caret.setStart(nextRow);
        else if (prevRow) this.caret.setEnd(prevRow);
        else this.deleteTable();
      }
    },
    deleteTable: function () {
      var table = this._getTable();
      if (table) {
        this.component.remove(table);
      }
    },
    // private
    _getTable: function () {
      var current = this.selection.getCurrent();
      var data = this.inspector.parse(current);
      if (data.isTable()) {
        return data.getTable();
      }
    },
    _getComponent: function () {
      var current = this.selection.getCurrent();
      var data = this.inspector.parse(current);
      if (data.isTable()) {
        var table = data.getTable();
        return this.component.create('table', table);
      }
    },
    _observeDropdown: function (dropdown) {
      var table = this._getTable();
      var items = dropdown.getItemsByClass('redactor-table-item-observable');
      var tableItem = dropdown.getItem('insert-table');
      if (table) {
        this._observeItems(items, 'enable');
        tableItem.disable();
      } else {
        this._observeItems(items, 'disable');
        tableItem.enable();
      }
    },
    _observeItems: function (items, type) {
      for (var i = 0; i < items.length; i++) {
        items[i][type]();
      }
    }
  });

  $R.add('class', 'table.component', {
    mixins: ['dom', 'component'],
    init: function (app, el) {
      this.app = app;
      // init
      return (el && el.cmnt !== undefined) ? el : this._init(el);
    },
    // public
    addHead: function () {
      this.removeHead();
      var columns = this.$element.find('tr')
        .first()
        .children('td, th')
        .length;
      var $head = $R.dom('<thead>');
      var $row = this._buildRow(columns, '<th>');
      $head.append($row);
      this.$element.prepend($head);
    },
    addRow: function (columns) {
      var $row = this._buildRow(columns);
      this.$element.append($row);
      return $row;
    },
    addRowTo: function (current, type) {
      return this._addRowTo(current, type);
    },
    addColumnTo: function (current, type) {
      var $current = $R.dom(current);
      var $currentRow = $current.closest('tr');
      var $currentCell = $current.closest('td, th');
      var index = 0;
      $currentRow.find('td, th')
        .each(function (node, i) {
          if (node === $currentCell.get()) index = i;
        });
      this.$element.find('tr')
        .each(function (node) {
          var $node = $R.dom(node);
          var origCell = $node.find('td, th')
            .get(index);
          var $origCell = $R.dom(origCell);
          var $td = $origCell.clone();
          $td.html('');
          if (type === 'right') $origCell.after($td);
          else $origCell.before($td);
        });
    },
    removeHead: function () {
      var $head = this.$element.find('thead');
      if ($head.length !== 0) $head.remove();
    },
    removeRow: function (current) {
      var $current = $R.dom(current);
      var $currentRow = $current.closest('tr');
      $currentRow.remove();
    },
    removeColumn: function (current) {
      var $current = $R.dom(current);
      var $currentRow = $current.closest('tr');
      var $currentCell = $current.closest('td, th');
      var index = 0;
      $currentRow.find('td, th')
        .each(function (node, i) {
          if (node === $currentCell.get()) index = i;
        });
      this.$element.find('tr')
        .each(function (node) {
          var $node = $R.dom(node);
          var origCell = $node.find('td, th')
            .get(index);
          var $origCell = $R.dom(origCell);
          $origCell.remove();
        });
    },
    // private
    _init: function (el) {
      var wrapper, element;
      if (typeof el !== 'undefined') {
        var $node = $R.dom(el);
        var node = $node.get();
        var $figure = $node.closest('figure');
        if ($figure.length !== 0) {
          wrapper = $figure;
          element = $figure.find('table')
            .get();
        } else if (node.tagName === 'TABLE') {
          element = node;
        }
      }
      this._buildWrapper(wrapper);
      this._buildElement(element);
      this._initWrapper();
    },
    _addRowTo: function (current, position) {
      var $current = $R.dom(current);
      var $currentRow = $current.closest('tr');
      if ($currentRow.length !== 0) {
        var columns = $currentRow.children('td, th')
          .length;
        var $newRow = this._buildRow(columns);
        $currentRow[position]($newRow);
        return $newRow;
      }
    },
    _buildRow: function (columns, tag) {
      tag = tag || '<td>';
      var $row = $R.dom('<tr>');
      for (var i = 0; i < columns; i++) {
        var $cell = $R.dom(tag);
        $cell.attr('contenteditable', true);
        $row.append($cell);
      }
      return $row;
    },
    _buildElement: function (node) {
      if (node) {
        this.$element = $R.dom(node);
      } else {
        this.$element = $R.dom('<table>');
        this.append(this.$element);
      }
    },
    _buildWrapper: function (node) {
      node = node || '<figure>';
      this.parse(node);
    },
    _initWrapper: function () {
      this.addClass('redactor-component');
      this.attr({
        'data-redactor-type': 'table',
        'tabindex': '-1',
        'contenteditable': false
      });
    }
  });

  $R.add('plugin', 'imagemanager', {
    translations: {
      en: {
        "choose": "Choose"
      }
    },
    init: function (app) {
      this.app = app;
      this.lang = app.lang;
      this.opts = app.opts;
    },
    // messages
    onmodal: {
      image: {
        open: function ($modal, $form) {
          if (!this.opts.imageManagerJson) return;
          this._load($modal)
        }
      }
    },
    // private
    _load: function ($modal) {
      var $body = $modal.getBody();
      this.$box = $R.dom('<div>');
      this.$box.attr('data-title', this.lang.get('choose'));
      this.$box.addClass('redactor-modal-tab');
      this.$box.hide();
      this.$box.css({
        overflow: 'auto',
        height: '300px',
        'line-height': 1
      });
      $body.append(this.$box);
      $R.ajax.get({
        url: this.opts.imageManagerJson,
        success: this._parse.bind(this)
      });
    },
    _parse: function (data) {
      for (var key in data) {
        var obj = data[key];
        if (typeof obj !== 'object') continue;
        var $img = $R.dom('<img>');
        var url = (obj.thumb) ? obj.thumb : obj.url;
        $img.attr('src', url);
        $img.attr('data-params', encodeURI(JSON.stringify(obj)));
        $img.css({
          width: '96px',
          height: '72px',
          margin: '0 4px 2px 0',
          cursor: 'pointer'
        });
        $img.on('click', this._insert.bind(this));
        this.$box.append($img);
      }
    },
    _insert: function (e) {
      e.preventDefault();
      var $el = $R.dom(e.target);
      var data = JSON.parse(decodeURI($el.attr('data-params')));
      this.app.api('module.image.insert', {
        image: data
      });
    }
  });

  $R.add('plugin', 'fullscreen', {
    translations: {
      en: {
        "fullscreen": "Fullscreen"
      }
    },
    init: function (app) {
      this.app = app;
      this.opts = app.opts;
      this.lang = app.lang;
      this.$win = app.$win;
      this.$doc = app.$doc;
      this.$body = app.$body;
      this.editor = app.editor;
      this.toolbar = app.toolbar;
      this.container = app.container;
      this.selection = app.selection;
      // local
      this.isOpen = false;
      this.docScroll = 0;
    },
    // public
    start: function () {
      var data = {
        title: this.lang.get('fullscreen'),
        api: 'plugin.fullscreen.toggle'
      };
      var button = this.toolbar.addButtonAuto('fullscreen', data);
      button.setIcon('<i class="re-icon-expand"></i>');
      button.addClass('pull-right');
      this.$target = (this.toolbar.isTarget()) ? this.toolbar.getTargetElement() : this.$body;
      if (this.opts.fullscreen) this.toggle();
    },
    toggle: function () {
      return (this.isOpen) ? this.close() : this.open();
    },
    open: function () {
      this.docScroll = this.$doc.scrollTop();
      this._createPlacemarker();
      this.selection.save();
      var $container = this.container.getElement();
      var $editor = this.editor.getElement();
      var $html = (this.toolbar.isTarget()) ? $R.dom('body, html') : this.$target;
      if (this.opts.toolbarExternal) this._buildInternalToolbar();
      this.$target.prepend($container);
      this.$target.addClass('redactor-body-fullscreen');
      $container.addClass('redactor-box-fullscreen');
      if (this.isTarget) $container.addClass('redactor-box-fullscreen-target');
      $html.css('overflow', 'hidden');
      if (this.opts.maxHeight) $editor.css('max-height', '');
      if (this.opts.minHeight) $editor.css('min-height', '');
      if (this.opts.maxWidth) {
        this.toolbar.$wrapper.css({'max-width': this.opts.maxWidth, 'margin': 'auto'});
      }
      this._resize();
      this.$win.on('resize.redactor-plugin-fullscreen', this._resize.bind(this));
      this.$doc.scrollTop(0);
      var button = this.toolbar.getButton('fullscreen');
      button.setIcon('<i class="re-icon-retract"></i>');
      this.selection.restore();
      this.isOpen = true;
      this.opts.zindex = 1051;
    },
    close: function () {
      this.isOpen = false;
      this.opts.zindex = false;
      this.selection.save();
      var $container = this.container.getElement();
      var $editor = this.editor.getElement();
      var $html = $R.dom('body, html');
      if (this.opts.toolbarExternal) this._buildExternalToolbar();
      this.$target.removeClass('redactor-body-fullscreen');
      this.$win.off('resize.redactor-plugin-fullscreen');
      $html.css('overflow', '');
      $container.removeClass('redactor-box-fullscreen redactor-box-fullscreen-target');
      $editor.css('height', 'auto');
      if (this.opts.minHeight) $editor.css('minHeight', this.opts.minHeight);
      if (this.opts.maxHeight) $editor.css('maxHeight', this.opts.maxHeight);
      if (this.opts.maxWidth) this.toolbar.$wrapper.css('max-width', '');
      var button = this.toolbar.getButton('fullscreen');
      button.setIcon('<i class="re-icon-expand"></i>');
      this._removePlacemarker($container);
      this.selection.restore();
      this.$doc.scrollTop(this.docScroll);
    },
    // private
    _resize: function () {
      var $toolbar = this.toolbar.getElement();
      var $editor = this.editor.getElement();
      var height = this.$win.height() - $toolbar.height();
      $editor.height(height);
    },
    _buildInternalToolbar: function () {
      var $wrapper = this.toolbar.getWrapper();
      var $toolbar = this.toolbar.getElement();
      $wrapper.addClass('redactor-toolbar-wrapper');
      $wrapper.append($toolbar);
      $toolbar.removeClass('redactor-toolbar-external');
      $container.prepend($wrapper);
    },
    _buildExternalToolbar: function () {
      var $wrapper = this.toolbar.getWrapper();
      var $toolbar = this.toolbar.getElement();
      this.$external = $R.dom(this.opts.toolbarExternal);
      $toolbar.addClass('redactor-toolbar-external');
      this.$external.append($toolbar);
      $wrapper.remove();
    },
    _createPlacemarker: function () {
      var $container = this.container.getElement();
      this.$placemarker = $R.dom('<span />');
      $container.after(this.$placemarker);
    },
    _removePlacemarker: function ($container) {
      this.$placemarker.before($container);
      this.$placemarker.remove();
    }
  });

  $R.add('plugin', 'video', {
    translations: {
      en: {
        "video": "Video",
        "video-html-code": "Video Embed Code or Youtube/Vimeo Link"
      }
    },
    modals: {
      'video':
        '<form action=""> \
           <div class="form-item"> \
             <label for="modal-video-input">## video-html-code ## <span class="req">*</span></label> \
             <textarea id="modal-video-input" name="video" style="height: 160px;"></textarea> \
           </div> \
         </form>'
    },
    init: function (app) {
      this.app = app;
      this.lang = app.lang;
      this.opts = app.opts;
      this.toolbar = app.toolbar;
      this.component = app.component;
      this.insertion = app.insertion;
      this.inspector = app.inspector;
    },
    // messages
    onmodal: {
      video: {
        opened: function ($modal, $form) {
          $form.getField('video')
            .focus();
        },
        insert: function ($modal, $form) {
          var data = $form.getData();
          this._insert(data);
        }
      }
    },
    oncontextbar: function (e, contextbar) {
      var data = this.inspector.parse(e.target)
      if (data.isComponentType('video')) {
        var node = data.getComponent();
        var buttons = {
          "remove": {
            title: this.lang.get('delete'),
            api: 'plugin.video.remove',
            args: node
          }
        };
        contextbar.set(e, node, buttons, 'bottom');
      }
    },
    // public
    start: function () {
      var obj = {
        title: this.lang.get('video'),
        api: 'plugin.video.open'
      };
      var $button = this.toolbar.addButtonAfter('image', 'video', obj);
      $button.setIcon('<i class="re-icon-video"></i>');
    },
    open: function () {
      var options = {
        title: this.lang.get('video'),
        width: '600px',
        name: 'video',
        handle: 'insert',
        commands: {
          insert: {
            title: this.lang.get('insert')
          },
          cancel: {
            title: this.lang.get('cancel')
          }
        }
      };
      this.app.api('module.modal.build', options);
    },
    remove: function (node) {
      this.component.remove(node);
    },
    // private
    _insert: function (data) {
      this.app.api('module.modal.close');
      if (data.video.trim() === '') {
        return;
      }
      // parsing
      data.video = this._matchData(data.video);
      // inserting
      if (this._isVideoIframe(data.video)) {
        var $video = this.component.create('video', data.video);
        this.insertion.insertHtml($video);
      }
    },
    _isVideoIframe: function (data) {
      return (data.match(/<iframe|<video/gi) !== null);
    },
    _matchData: function (data) {
      var iframeStart = '<iframe style="width: 500px; height: 281px;" src="';
      var iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
      if (this._isVideoIframe(data)) {
        var allowed = ['iframe', 'video', 'source'];
        var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
        data = data.replace(tags, function ($0, $1) {
          return (allowed.indexOf($1.toLowerCase()) === -1) ? '' : $0;
        });
      }
      if (data.match(this.opts.regex.youtube)) {
        data = data.replace(this.opts.regex.youtube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd);
      } else if (data.match(this.opts.regex.vimeo)) {
        data = data.replace(this.opts.regex.vimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd);
      }
      return data;
    }
  });
  $R.add('class', 'video.component', {
    mixins: ['dom', 'component'],
    init: function (app, el) {
      this.app = app;
      // init
      return (el && el.cmnt !== undefined) ? el : this._init(el);
    },
    // private
    _init: function (el) {
      if (typeof el !== 'undefined') {
        var $node = $R.dom(el);
        var $wrapper = $node.closest('figure');
        if ($wrapper.length !== 0) {
          this.parse($wrapper);
        } else {
          this.parse('<figure>');
          this.append(el);
        }
      } else {
        this.parse('<figure>');
      }
      this._initWrapper();
    },
    _initWrapper: function () {
      this.addClass('redactor-component');
      this.attr({
        'data-redactor-type': 'video',
        'tabindex': '-1',
        'contenteditable': false
      });
    }
  });
  $R.add('plugin', 'textdirection', {
    translations: {
      en: {
        "change-text-direction": "RTL-LTR",
        "left-to-right": "Left to Right",
        "right-to-left": "Right to Left"
      }
    },
    init: function(app) {
      this.app = app;
      this.lang = app.lang;
      this.block = app.block;
      this.toolbar = app.toolbar;
      this.selection = app.selection;
    },
    // public
    start: function()
    {
      var dropdown = {};

      dropdown.ltr = { title: this.lang.get('left-to-right'), api: 'plugin.textdirection.set', args: 'ltr' };
      dropdown.rtl = { title: this.lang.get('right-to-left'), api: 'plugin.textdirection.set', args: 'rtl' };

      var $button = this.toolbar.addButton('textdirection', { title: this.lang.get('change-text-direction') });
      $button.setIcon('<i class="re-icon-textdirection"></i>');
      $button.setDropdown(dropdown);
    },
    set: function(type) {
      var block = this.selection.getBlock();
      if (block && block.tagName === 'LI') {
        var list = $R.dom(block).parents('ul, ol', '.redactor-in').last();
        this.block.add({ attr: { dir: type }}, false, list);
      }
      else {
        this.block.add({ attr: { dir: type }});
      }
    }
  });

  // Monkey patch context bar to have ability to add a button
  var contextbar = $R[$R.env['module']]['contextbar'];
  $R.add('module', 'contextbar', $R.extend(contextbar.prototype, {
    append: function(e, button) {
      var $btn = $R.create('contextbar.button', this.app, button);
      if ($btn.html() !== '')
      {
          this.$contextbar.append($btn);
      }
      var pos = this._buildPosition(e, this.$el);
      this.$contextbar.css(pos);
    }
  }));

  $R.add('plugin', 'imageannotate', {
    init: function(app) {
        this.app = app;
        this.lang = app.lang;
        this.colors = [
          '#ffffff', '#888888', '#000000', 'fuchsia', 'blue', 'red',
          'lime', 'blueviolet', 'cyan', '#f4a63b', 'yellow']
        .concat(app.instances.fontcolor.colors.slice(11));
        this.loadedFabric = false;
    },
    modals: {
        'annotate':
            '<div class="toolbar" style="margin-top:-24px;margin-bottom:10px"></div>'
           +'<div class="canvas" style="position:relative"></div>',
    },
    onmodal: {
        annotate: {
            open: function($modal, $form) {
                this._build($modal);
            },
            commit: function ($modal, $form) {
                this.commit();
            }
        },
    },
    oncontextbar: function(e, contextbar) {
        var current = this.app.selection.getCurrent();
        var data = this.app.inspector.parse(current);

        if (!data.isFigcaption() && data.isComponentType('image')) {
          contextbar.append(e, {
            title: __('Annotate'),
            api: 'plugin.imageannotate.startAnnotate',
            args: [data.getComponent()]
          });
        }
    },
    _build: function($modal) {
        var $body = $modal.getBody();
        this._buildToolbar($body.find('.toolbar'));

        this.$image = $R.dom(this.image).find('img');
        canvas = this.initCanvas(this.$image, $body);
    },
    startAnnotate: function(img) {
        this.image = img;
        var that=this;
        if (!this.loadedFabric) {
            getConfig().then(function(c) {
                $.getScript(c.path + 'js/fabric.min.js', function() {
                    that.loadedFabric = true;
                });
            });
        }
        var options = {
            title: __('Annotate Image'),
            width: '850px',
            name: 'annotate',
            handle: 'commit',
            commands: {
              commit: {
                title: __('Commit')
              },
              cancel: {
                title: this.lang.get('cancel')
              }
            }
        };

        // Await loading of Fabric framework
        var that = this, I = setInterval(function() {
            if (that.loadedFabric) {
                clearInterval(I);
                that.app.api('module.modal.build', options);
            }
        }, 25);
    },
    teardownAnnotate: function() {
        var state = this.canvas.toObject(), places = 2;
        // Capture current annotations
        delete state.backgroundImage;
        this.$image.attr('data-annotations',
            btoa(JSON.stringify(state, function(key, value) {
                // limit precision of floats
                if (typeof value === 'number') {
                    return parseFloat(value.toFixed(places));
                }
                return value;
            }))
        );
        this.app.api('module.modal.close');
    },
    _buildToolbar: function($body) {
        var T = this.toolbar = $R.create('toolbar', this.app);
        T.getWrapper().addClass('redactor-toolbar-wrapper');
        T.getElement().addClass('redactor-toolbar');
        $body.append(T.getWrapper());

        var $button = T.addButton('drawshape', {
            title: __('Add Shape'),
            dropdown: {
                arrow: {
                    title: '<span><i class="icon-long-arrow-right icon-fixed-width"></i> {}</span>'
                        .replace('{}', __('Add Arrow')),
                    api: 'plugin.imageannotate.drawArrow'
                },
                box: {
                    title: '<span><i class="icon-check-empty icon-fixed-width"></i> {}</span>'
                        .replace('{}', __('Add Box')),
                    api: 'plugin.imageannotate.drawBox'
                },
                ellipse: {
                    title: '<span><i class="icon-circle-blank icon-fixed-width"></i> {}</span>'
                        .replace('{}', __('Add Ellipse')),
                    api: 'plugin.imageannotate.drawEllipse'
                },
                text: {
                    title: '<span><i class="icon-font icon-fixed-width"></i> {}</span>'
                        .replace('{}', __('Add Text')),
                    api: 'plugin.imageannotate.drawText'
                },
                scribble: {
                    title: '<span><i class="icon-pencil icon-fixed-width"></i> {}</span>'
                        .replace('{}', __('Add Scribble')),
                    api: 'plugin.imageannotate.drawFree'
                },
            },
            icon: '<i class="icon-plus"></i>',
        }, 'first');

        T.addButton('sel_color', {
            title: __('Shape Color'),
            icon: '<i class="icon-tint"></i>',
            dropdown: this._buildDropdown(),
        });

        var dropdown = {}, sizes = [15,20,25,30,40,50];
        sizes.forEach(function(i) {
            var text = '' + i + 'px';
            dropdown['size' + i] = {
                title: $R.dom('<span>').css('font-size', '' + i + 'px').text(text).get().outerHTML,
                api: 'plugin.imageannotate.setFontSize',
                args: i,
            };
        });
        var $button = T.addButton('sel_textsize', {
            title: __('Font Size'),
            icon: '<i class="icon-text-height"></i>',
            dropdown: dropdown,
        });

        var dropdown = {};
        var fonts = ['Arial', 'Times New Roman', 'Monospace', 'Fantasy', 'Cursive'];
        fonts.forEach(function(i) {
            dropdown[i] = {
                title: $R.dom('<span>').css('font-family', i).text(i).get().outerHTML,
                api: 'plugin.imageannotate.setFontFamily',
                args: i,
            };
        });
        $button = T.addButton('sel_fontfamily', {
            title: __('Font Family'),
            icon: '<i class="icon-font"></i>',
            dropdown: dropdown,
        });

        var $button = T.addButton('sel_strokewidth', {
            title: __('Outline'),
            icon: '<i class="icon-check-empty"></i>',
        });
        var dropdown = {}, sizes = [0,2.5,5,7.5,10,15];
        sizes.forEach(function(i) {
            var text = '' + i + 'px';
            dropdown['size' + i] = {
                title: $R.dom('<span>').css('border-left', '' + i + 'px solid black')
                    .append($R.dom('<span>').text(text))
                    .get().outerHTML,
                api: 'plugin.imageannotate.setStrokeWidth',
                args: i,
            };
        });
        $button.setDropdown(dropdown);

        T.addButton('sel_layerup', {
            title: __('Bring Forward'),
            icon: '<i class="icon-double-angle-up"></i>',
            api: 'plugin.imageannotate.bringForward',
        });
        T.addButton('sel_opacity', {
            title: __('Toggle Opacity'),
            icon: '<i class="icon-eye-close"></i>',
            api: 'plugin.imageannotate.setOpacity',
        });

        T.addButton('sel_trash', {
            title: __('Delete Shape'),
            icon: '<i class="icon-trash"></i>',
            api: 'plugin.imageannotate.discard',
        });

        this.disableContextualButtons();
    },

    updateSelection: function() {
        if (this.canvas.getActiveObjects().length > 0)
            this.enableContextualButtons();
        else
            this.disableContextualButtons();
    },

    enableContextualButtons: function() {
        this.toolbar.getButtons().forEach(function(b) {
            if (b.name.indexOf('sel_') == 0)
                b.enable();
        });
    },

    disableContextualButtons: function() {
        this.toolbar.getButtons().forEach(function(b) {
            if (b.name.indexOf('sel_') == 0)
                b.disable();
        });
    },

    _setColor: function(attr, color) {
      $.each(this.canvas.getActiveObjects(), function() {
          this.set(attr, color);
      });
      this.canvas.renderAll();
    },

    _removeColor: function(attr) {
      $.each(this.canvas.getActiveObjects(), function() {
          this.set(attr, 'rgba(0, 0, 0, 0)');
      });
      this.canvas.renderAll();
    },

    // Color picker -- copied from `fontcolor` plugin
    _buildDropdown: function () {
      var $dropdown = $R.dom('<div class="redactor-dropdown-cells">');
      this.$selector = this._buildSelector();
      this.$selectorFill = this._buildSelectorItem('fill', __('Fill'));
      this.$selectorFill.addClass('active');
      this.$selectorStroke = this._buildSelectorItem('stroke', __('Outline'));
      this.$selector.append(this.$selectorFill);
      this.$selector.append(this.$selectorStroke);
      this.$pickerFill = this._buildPicker('fill');
      this.$pickerStroke = this._buildPicker('stroke');
      $dropdown.append(this.$selector);
      $dropdown.append(this.$pickerFill);
      $dropdown.append(this.$pickerStroke);
      this._buildSelectorEvents();
      $dropdown.width(242);
      return $dropdown;
    },
    _buildSelector: function () {
      var $selector = $R.dom('<div>');
      $selector.addClass('redactor-dropdown-selector');
      return $selector;
    },
    _buildSelectorItem: function (name, title) {
      var $item = $R.dom('<span>');
      $item.attr('rel', name)
        .html(title);
      $item.addClass('redactor-dropdown-not-close');
      return $item;
    },
    _buildSelectorEvents: function () {
      this.$selectorFill.on('mousedown', function (e) {
        e.preventDefault();
        this.$selector.find('span')
          .removeClass('active');
        this.$pickerStroke.hide();
        this.$pickerFill.show();
        this.$selectorFill.addClass('active');
      }.bind(this));
      this.$selectorStroke.on('mousedown', function (e) {
        e.preventDefault();
        this.$selector.find('span')
          .removeClass('active');
        this.$pickerFill.hide();
        this.$pickerStroke.show();
        this.$selectorStroke.addClass('active');
      }.bind(this));
    },
    _buildPicker: function (name) {
      var $box = $R.dom('<div class="re-dropdown-box-' + name + '">');
      var len = this.colors.length;
      var self = this;
      var func = function (e) {
        e.preventDefault();
        var $el = $R.dom(e.target);
        self._setColor($el.data('rule'), $el.attr('rel'));
      };
      for (var z = 0; z < len; z++) {
        var color = this.colors[z];
        var $swatch = $R.dom('<span>');
        $swatch.attr({
          'rel': color,
          'data-rule': name
        });
        $swatch.css({
          'background-color': color,
          'font-size': 0,
          'border': '2px solid #fff',
          'width': '22px',
          'height': '22px'
        });
        $swatch.on('mousedown', func);
        $box.append($swatch);
      }
      var $el = $R.dom('<a>');
      $el.attr({
        'href': '#'
      });
      $el.css({
        'display': 'block',
        'clear': 'both',
        'padding': '8px 5px',
        'font-size': '12px',
        'line-height': 1
      });
      $el.html(this.lang.get('none'));
      $el.on('click', function (e) {
        e.preventDefault();
        self._removeColor(name);
      });
      $box.append($el);
      if (name == 'stroke') $box.hide();
      return $box;
    },

    // Shapes
    drawShape: function(ondown, onmove, onup, cursor) {
      // @see http://jsfiddle.net/URWru/
      var fcanvas = this.canvas,
          scale = this.scale,
          isDown, shape,
          that = this,
          mousedown = function(o) {
            isDown = true;
            that.app.api('module.buffer.trigger');
            var pointer = fcanvas.getPointer(o.e);
            shape = ondown(pointer, o.e);
            if (shape) fcanvas.add(shape);
          },
          mousemove = function(o) {
            if (!isDown) return;
            onmove(shape, fcanvas.getPointer(o.e), o.e);
            fcanvas.requestRenderAll();
          },
          mouseup = function(o) {
            isDown = false;
            if (onup) {
              if (shape2 = onup(shape, fcanvas.getPointer(o.e))) {
                shape.remove();
                fcanvas.add(shape2);
                shape = shape2;
              }
            }
            if (shape) shape.setCoords()
            fcanvas.calcOffset()
              .off('mouse:down', mousedown)
              .off('mouse:up', mouseup)
              .off('mouse:move', mousemove)
              .discardActiveObject()
              .renderAll();

            if (shape) fcanvas.setActiveObject(shape);
            fcanvas.selection = true;
            fcanvas.defaultCursor = 'default';
          };

        fcanvas.selection = false;
        fcanvas.defaultCursor = cursor || 'crosshair';
        // Ensure double presses of same button are squelched
        fcanvas.off('mouse:down');
        fcanvas.off('mouse:up');
        fcanvas.off('mouse:move');
        fcanvas.on('mouse:down', mousedown);
        fcanvas.on('mouse:up', mouseup);
        fcanvas.on('mouse:move', mousemove);
        return false;
    },

    drawFree: function() {
      var scale = this.scale, fcanvas = this.canvas;
      fcanvas.isDrawingMode = true;
      fcanvas.freeDrawingBrush = new fabric.PencilBrush(fcanvas)
      fcanvas.freeDrawingBrush.width = 5 * scale;
      fcanvas.freeDrawingBrush.color = 'red';
      return this.drawShape(
          function() {},
          function() {},
          function(shape, pointer, event) {
              fcanvas.isDrawingMode = false;
          }
      );
    },

    drawArrow: function() {
      var top, left, scale = this.scale
      return this.drawShape(
        function(pointer) {
          top = pointer.y;
          left = pointer.x;
          return new fabric.Group([
            new fabric.Line([0, 5 * scale, 0, 5 * scale], {
              strokeWidth: 5 * scale,
              fill: 'red',
              stroke: 'red',
              originX: 'center',
              originY: 'center',
              selectable: false,
              hasBorders: false
            }),
            new fabric.Polygon([
              {x: 20 * scale, y: 0},
              {x: 0, y: -5 * scale},
              {x: 0, y: 5 * scale}
              ], {
              strokeWidth: 0,
              fill: 'red',
              originX: 'center',
              originY: 'center',
              selectable: false,
              hasBorders: false
            })
          ], {
            left: pointer.x,
            top: pointer.y,
            originX: 'center',
            originY: 'center'
          });
        },
        function(group, pointer) {
          var dx = pointer.x - left,
              dy = pointer.y - top,
              angle = Math.atan(dy / dx),
              d = Math.sqrt(dx * dx + dy * dy) - 10,
              sign = dx < 0 ? -1 : 1,
              dy2 = Math.sin(angle) * d * sign;
              dx2 = Math.cos(angle) * d * sign,
          group.item(0)
            .set({ x2: dx2, y2: dy2 });
          group.item(1)
            .set({
              angle: angle * 180 / Math.PI,
              flipX: dx < 0,
              flipY: dy < 0
            })
            .setPositionByOrigin(new fabric.Point(dx, dy),
                'center', 'center');
        },
        function(shape, pointer) {
          var dx = pointer.x - left,
              dy = pointer.y - top,
              angle = Math.atan(dy / dx),
              d = Math.sqrt(dx * dx + dy * dy);
          // Mess with the next two lines and you *will* be sorry!
          shape.forEachObject(function(e) { shape.removeWithUpdate(e); });
          return new fabric.Path(
            'M '+left+' '+top+' l '+(d-15*scale)+' 0 0 -a b a -b a 0 -a z'
              .replace(/a/g, 3 * scale).replace(/b/g, 15 * scale),
            {
            angle: angle * 180 / Math.PI + (dx < 0 ? 180 : 0),
            strokeWidth: 5 * scale,
            fill: 'red',
            stroke: 'red'
          });
        }
      );
    },

    drawEllipse: function() {
      var scale = this.scale
      return this.drawShape(
        function(pointer) {
          return new fabric.Ellipse({
            top: pointer.y,
            left: pointer.x,
            strokeWidth: 5 * scale,
            fill: 'transparent',
            stroke: 'red',
            originX: 'left',
            originY: 'top'
          });
        },
        function(circle, pointer, event) {
          var x = circle.get('left'), y = circle.get('top'),
              dx = pointer.x - x, dy = pointer.y - y,
              sw = circle.strokeWidth / 2;
          // Use SHIFT to draw circles
          if (event.shiftKey) {
            dy = dx = Math.max(dx, dy);
          }
          circle.set({
            rx: Math.max(0, Math.abs(dx/2) - sw),
            ry: Math.max(0, Math.abs(dy/2) - sw),
            originX: dx < 0 ? 'right' : 'left',
            originY: dy < 0 ? 'bottom' : 'top'});
        }
      );
    },

    drawBox: function() {
      var scale = this.scale
      return this.drawShape(
        function(pointer) {
          return new fabric.Rect({
            top: pointer.y,
            left: pointer.x,
            strokeWidth: 5 * scale,
            fill: 'transparent',
            stroke: 'red',
            originX: 'left',
            originY: 'top'
          });
        },
        function(rect, pointer, event) {
          var x = rect.get('left'), y = rect.get('top'),
              dx = pointer.x - x, dy = pointer.y - y;
          // Use SHIFT to draw squares
          if (event.shiftKey) {
            dy = dx = Math.max(dx, dy);
          }
          rect.set({ width: Math.abs(dx), height: Math.abs(dy),
            originX: dx < 0 ? 'right' : 'left',
            originY: dy < 0 ? 'bottom' : 'top'});
        }
      );
    },

    drawText: function() {
      var fcanvas = this.canvas, scale = this.scale;
      return this.drawShape(
        function(pointer) {
          return new fabric.IText(__('Text'), {
            top: pointer.y,
            left: pointer.x,
            fill: 'red',
            originX: 'left',
            originY: 'top',
            fontFamily: 'sans-serif',
            fontSize: 30 * scale
          });
        },
        function(rect, pointer, event) {
          rect.set({width: 75 * scale, height: 30 * scale,
            originX: 'left', originY: 'top'});
        },
        function(shape) {
          shape.on('editing:exited', function() {
            if (!shape.get('text'))
              fcanvas.remove(shape);
          });
        },
        'text'
      );
    },

    // Action buttons
    _setFont: function(attr, value) {
        var obj = {};
        obj[attr] = value;
        $.each(this.canvas.getActiveObjects(), function(element) {
            if (this instanceof fabric.IText) {
                if (this.getSelectedText()) {
                    this.setSelectionStyles(obj);
                }
                else {
                    this.set(attr, value);
                }
            }
        });
        this.canvas.renderAll();
    },

    setFontSize: function(pixels) {
        var scale = this.scale;
        this._setFont('fontSize', pixels * scale);
    },

    setFontFamily: function(name) {
        this._setFont('fontFamily', name);
    },

    setStrokeWidth: function(pixels) {
        var scale = this.scale;
        $.each(this.canvas.getActiveObjects(), function() {
            this.set('strokeWidth', pixels * scale);
        });
        this.canvas.renderAll();
    },

    setOpacity: function() {
        $.each(this.canvas.getActiveObjects(), function() {
            if (this.get('opacity') != 1)
                this.set('opacity', 1);
            else
                this.set('opacity', 0.6);
        });
        this.canvas.renderAll();
    },

    bringForward: function(e) {
        $.each(this.canvas.getActiveObjects(), function() {
            this.bringForward();
        });
    },

    keydown: function(e) {
        // Check if [delete] was pressed with selected objects
        if (e.keyCode == 8 || e.keyCode == 46)
            return this.discard(e);
        // Revert to previous state for CTRL+Z or CMD+Z
        else if (e.keyCode == 90 && (e.metaKey || e.ctrlKey)) {
            this.canvas.loadFromJSON(atob(this.$image.attr('data-annotations')));
            return false;
        }
    },

    discard: function(e) {
        this.setBuffer();
        var that = this;
        $.each(this.canvas.getActiveObjects(), function() {
            that.canvas.remove(this); }
        );
        this.canvas.renderAll();
    },

    commit: function() {
        this.canvas.discardActiveObject();

        // Upload the annotated image to server
        this.app.api('module.buffer.trigger');
        this.setBuffer();
        var annotated = this.canvas.toDataURL({
              format: 'jpg', quality: 4,
              multiplier: 1 / this.canvas.getZoom()
            }),
            file = new Blob([annotated], {type: 'image/jpeg'});

        // Fallback to the data URL — show while the image is being uploaded
        this.origSrc = this.$image.attr('src');
        this.$image.attr('src', annotated);

        this.teardownAnnotate();
        this.app.api('module.upload.send', {
            url: this.app.opts.imageUpload,
            data: this.app.opts.imageData,
            paramName: this.app.opts.imageUploadParam,
            name: 'imageannotate',
            files: [file],
        });

        this.toolbar.destroy();
        return false;
    },

    // Fired from module.upload.send() after completion
    onupload: {
        imageannotate: {
            complete: function(response) {
                response.imageannotate = true;
                this.app.api('module.image.insert', response);
            },
        },
    },
      
    onimage: {
        uploaded: function(image, response) {
            // This is called for all uploads, but we only care about the
            // ones initiated from this plugin
            if (!this.$image || !response.imageannotate)
                return;

            // After successful upload, replace the old image with the new one.
            // Transfer the annotation state to the new image for replay.
            var $image = $R.dom(image).find('img');
            // Transfer the annotation JSON data and drop the original image.
            $image.attr('data-annotations', this.$image.attr('data-annotations'));
            // Record the image that was originally annotated. If the committed
            // image is annotated again, it should be the original image with
            // the annotations placed live on the original image. The image
            // being committed here will be discarded.
            $image.attr('data-orig-annotated-image-src',
                this.$image.attr('data-orig-annotated-image-src') || this.origSrc
            );
            // Out with the old
            this.$image.remove();
        },
    },

    // Keep the scale at 1.0 so that the stroke size is not stretched when
    // the size and shape of the object is stretched.
    resizeShape: function(o) {
      var shape = o.target;
      if (shape instanceof fabric.Ellipse) {
        shape.set({
          rx: shape.get('rx') * shape.get('scaleX'),
          ry: shape.get('ry') * shape.get('scaleY'),
          scaleX: 1,
          scaleY: 1
        });
      }
      else if (shape instanceof fabric.Rect) {
        shape.set({
          width: shape.get('width') * shape.get('scaleX'),
          height: shape.get('height') * shape.get('scaleY'),
          scaleX: 1,
          scaleY: 1
        });
      }
    },
    setBuffer: function() {
      var state = this.canvas.toObject(), places = 2;
      // Capture current annotations
      delete state.backgroundImage;
      this.$image.attr('data-annotations',
          btoa(JSON.stringify(state, function(key, value) {
              // limit precision of floats
              if (typeof value === 'number') {
                  return parseFloat(value.toFixed(places));
              }
              return value;
          }))
      );
    },

    // Startup

    initCanvas: function($img, $body) {
      var canvas = $R.dom('<canvas>').css({
            width: '100%', height: '100%'
          });
      $body.find('.canvas').append(canvas);
      var fcanvas = new fabric.Canvas(canvas.get(), {
            backgroundColor: 'rgba(0,0,0,0)',
            includeDefaultValues: false,
            width: canvas.width(),
            height: canvas.height(),
          }),
          previous = $img.attr('data-annotations');

      // Catch [delete] key and map to delete object
      //self.opts.keydownCallback = this.keydown.bind(self);
      //self.opts.keyupCallback = this.keydown.bind(self);

      var I = new Image();
      I.src = $img.attr('src');
      // Determine the scaling adjustment to fit the image in the modal
      // dialog. Also note, that both the width and height should be
      // considered to ensure very tall images do not float off the
      // screen.
      var width = Math.min(canvas.width(), $img.width()),
          viewscale = width / $img.width(),
          height = $img.height() * viewscale,
          maxheight = $(window).height() - 300;

      if (height > maxheight) {
          viewscale *= maxheight / height;
          height = maxheight;
          width = $img.width() * viewscale;
      }

      var drawscale = width / I.width,
          scaleWidth = width / drawscale,
          scaleHeight = height / drawscale;
      this.scale = 1 / drawscale;

      // Set default control appearance for all objects
      fabric.Object.prototype.set({
        transparentCorners: false,
        borderColor: 'rgba(102,153,255,0.9)',
        cornerColor: 'rgba(102,153,255,0.5)',
        cornerSize: 8,
      });

      fcanvas
        .setDimensions({width: width, height: height})
        .setZoom(drawscale)
        .setBackgroundImage(
            $img.attr('data-orig-annotated-image-src') || $img.attr('src'),
            fcanvas.renderAll.bind(fcanvas), {
          width: scaleWidth,
          height: scaleHeight,
          // Needed to position overlayImage at 0/0
          originX: 'left',
          originY: 'top'
        })
        .on('object:scaling', this.resizeShape.bind(this))
        .on('selection:updated', this.updateSelection.bind(this))
        .on('selection:created', this.updateSelection.bind(this))
        .on('selection:cleared', this.updateSelection.bind(this));

      if (previous) {
        fcanvas.loadFromJSON(atob(previous));
      }

      this.canvas = fcanvas;
      return fcanvas;
    }
  });

  $R.add('plugin', 'contexttypeahead', {
    typeahead: false,
    context: false,
    variables: false,

    init: function(app) {
      this.app = app;
    },

    start: function() {
      this.$editor = this.app.editor.getElement();
      this.$element = $(this.app.rootElement);
      if (!this.$element.data('rootContext'))
        return;

      this.$editor.on('keyup', this.watch.bind(this));
      this.$editor.on('keydown', this.watch.bind(this));
      this.$editor.on('click', this.watch.bind(this));
    },

    watch: function(e) {
      var current = this.app.api('selection.getCurrent'),
          allText = this.$editor.text(),
          offset = this.app.api('offset.get', this.app.editor.$editor),
          lhs = allText.substring(0, offset.start),
          search = new RegExp(/%\{([^}]*)$/),
          match,
          e = $.Event(e);

      if (!lhs) {
        return !e.isDefaultPrevented();
      }

      if (e.which == 27 || !(match = search.exec(lhs)))
        // No longer in a element — close typeahead
        return this.destroy();

      if (e.type == 'click')
        return;

      // Locate the position of the cursor and the number of characters back
      // to the `%{` symbols
      var sel         = this.app.api('selection.get'),
          range       = sel.getRangeAt(0),
          content     = current.textContent,
          clientRects = range.getClientRects(),
          position    = clientRects[0],
          backText    = match[1],
          parent      = this.app.api('selection.getParent') || this.$element,
          that        = this;

      // Insert a hidden text input to receive the typed text and add a
      // typeahead widget
      if (!this.typeahead) {
        this.typeahead = $('<input type="text">')
          .css({position: 'absolute', visibility: 'hidden'})
          .width(0).height(position.height - 4)
          .appendTo(document.body)
          .typeahead({
            property: 'variable',
            minLength: 0,
            arrow: $('<span class="pull-right"><i class="icon-muted icon-chevron-right"></i></span>')
                .css('padding', '0 0 0 6px'),
            highlighter: function(variable, item) {
              var base = $.fn.typeahead.Constructor.prototype.highlighter
                    .call(this, variable),
                  further = new RegExp(variable + '\\.'),
                  extendable = Object.keys(that.variables).some(function(v) {
                    return v.match(further);
                  }),
                  arrow = extendable ? this.options.arrow.clone() : '';

              return $('<div/>').html(base).prepend(arrow).html()
                + $('<span class="faded">')
                  .text(' — ' + item.desc)
                  .wrap('<div>').parent().html();
            },
            item: '<li><a href="#" style="display:block"></a></li>',
            source: this.getContext.bind(this),
            sorter: function(items) {
              items.sort(
                function(a,b) {return a.variable > b.variable ? 1 : -1;}
              );
              return items;
            },
            matcher: function(item) {
              if (item.toLowerCase().indexOf(this.query.toLowerCase()) !== 0)
                return false;

              return (this.query.match(/\./g) || []).length == (item.match(/\./g) || []).length;
            },
            onselect: this.select.bind(this),
            scroll: true,
            items: 100
          });
      }

      if (position) {
        var width = this.textWidth(
              backText,
              this.app.api('selection.getParent') || $('<div class="redactor-editor">')
            ),
            pleft = $(parent).offset().left,
            left = position.left - width;

        if (left < pleft)
            // This is a bug in chrome, but I'm not sure how to adjust it
            left += pleft;

        this.typeahead
          .css({top: position.top + this.app.$win.scrollTop(), left: left});
      }

      this.typeahead
        .val(match[1])
        .triggerHandler(e);

      return !e.isDefaultPrevented();
    },

    getContext: function(typeahead, query) {
      var dfd, that=this,
          root = this.$element.data('rootContext');
      if (!this.context) {
        dfd = $.Deferred();
        $.ajax('ajax.php/content/context', {
          data: {root: root},
          success: function(json) {
            var items = $.map(json, function(v,k) {
              return {variable: k, desc: v};
            });
            that.variables = json;
            dfd.resolve(items);
          }
        });
        this.context = dfd;
      }
      // Only fetch the context once for this redactor box
      this.context.then(function(items) {
        typeahead.process(items);
      });
    },

    textWidth: function(text, clone) {
      var c = $(clone),
          o = c.clone().text(text)
            .css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden'})
            .css({'font-family': c.css('font-family'), 'font-weight': c.css('font-weight'),
              'font-size': c.css('font-size')})
            .appendTo($('body')),
          w = o.width();

      o.remove();

      return w;
    },

    destroy: function() {
      if (this.typeahead) {
        this.typeahead.typeahead('hide');
        this.typeahead.remove();
        this.typeahead = false;
      }
    },

    select: function(item, event) {
      // Collapse multiple textNodes together
      (this.app.api('selection.getBlock') || this.$element.get(0)).normalize();
      var current = this.app.api('selection.getCurrent'),
          sel     = this.app.api('selection.get'),
          range   = sel.getRangeAt(0),
          cursorAt = range.endOffset,
          // TODO: Consume immediately following `}` symbols
          search  = new RegExp(/%\{([^}]*)(\}?)$/);

      // FIXME: ENTER will end up here, but current will be empty

      if (!current)
        return;

      // Set cursor at the end of the expanded text
      var left = current.textContent.substring(0, cursorAt),
          right = current.textContent.substring(cursorAt),
          autoExpand = event.target.nodeName == 'I',
          selected = item.variable + (autoExpand ? '.' : '')
          newLeft = left.replace(search, '%{' + selected + '}');

      current.textContent = newLeft
        // Drop the remaining part of a variable block, if any
        + right.replace(/[^%}]*?[%}]/, '');

      range.setStart(current, newLeft.length - 1);
      range.setEnd(current, newLeft.length - 1);
      this.app.api('selection.setRange', range);
      if (!autoExpand)
          return this.destroy();

      this.typeahead.val(selected);
      this.typeahead.typeahead('lookup');
      return false;
    }
  });

  // Make the toolbar a class rather than a service, so it can be reused in
  // a dialog
  var ToolbarService = $R[$R.env['service']]['toolbar'];
  $R.add('class', 'toolbar', $R.extend(ToolbarService.prototype, {
    init: function(app) {
        this.app = app
        this._oldtoolbar = app.toolbar;
        this.app.toolbar = this;
        // Connect what's normally available in a service module
        this.opts = app.opts;
        this.detector = app.detector;

        this.buttons = [];
        this.dropdownOpened = false;
        this.buttonsObservers = {};

        // Start immediately
        this.create();
        this.$wrapper.append(this.$toolbar);
    },
    is: function() {
        return true;
    },
    destroy: function() {
        this.app.toolbar = this._oldtoolbar;
    }
  }));

  $R.add('plugin', 'translatable', {
    langs: undefined,
    config: undefined,
    current: undefined,
    primary: undefined,
    changed: {},

    init: function(app) {
      this.app = app;
      this.statusbar = app.statusbar;
      this.$textarea = $R.dom(this.app.rootElement);
      this.$editor = app.editor.getElement();
    },

    start: function() {
      this.fetch('ajax.php/i18n/langs/all')
        .then(this.setLangs.bind(this));
      getConfig().then(this.setConfig.bind(this));
      $editor = this.app.editor.getElement();
      this.translateTag = this.$textarea.data()['translateTag'];
    },

    setLangs: function(langs) {
      if (Object.keys(langs).length < 2)
        return;
      this.langs = langs;
      this.buildDropdown();
    },

    setConfig: function(config) {
      this.config = config;
      this.buildDropdown();
    },

    buildDropdown: function() {
      if (!this.config || !this.langs)
        return;

      var primary = this.$textarea,
          primary_lang = this.config.primary_language.replace('-','_'),
          primary_info = this.langs[primary_lang],
          items = {},
          dropdown = {
            primary_lang: {
              title: '<i class="flag flag-'+primary_info.flag+'"></i> '+primary_info.name,
              api: 'plugin.translatable.switchTo',
              args: primary_lang,
            },
          },
          button = this.app.toolbar.addButton('flag', {
            title: __('Translate'),
          });

      this.primary = this.current = primary_lang;
      this.button = button;

      $.each(this.langs, function(lang, info) {
        if (lang == primary_lang)
          return;
        dropdown[lang] = {
          title: '<i class="flag flag-'+info.flag+'"></i> '+info.name,
          api: 'plugin.translatable.switchTo',
          args: lang,
        };
      });

      // Add the button to the toolbar
      button.setDropdown(dropdown);

      // Flip back to primary language before submitting
      var that=this;
      this.app.editor.getElement().closest('form').on('submit', function() {
        that.switchTo(primary_lang);
      });

      this.showStatus(this.primary);
    },

    showStatus: function(lang) {
      var tstatus = $R.dom('<span>').text('lang: ')
      tstatus.append($R.dom('<i class="flag flag-'+this.langs[this.current].flag+'"></i>'))
      tstatus.append(document.createTextNode(' ' + this.current))
      this.statusbar.add('translatable', tstatus);

      this.button.setIcon('<i class="flag flag-'+this.langs[lang].flag+'"></i>');
    },

    switchTo: function(lang) {
      if (lang == this.current)
        return;

      var that = this;
      this.fetch('ajax.php/i18n/translate/' + this.translateTag)
        .then(function(json) {
          // Preserve current text
          json[that.current] = that.app.source.getCode();
          that.current = lang;
          that.app.insertion.set(json[lang] || '', false, true);
          that.app.api('module.source.sync');

          that.app.editor.getElement()
            .attr({lang: lang, dir: that.langs[lang].direction});

          that.showStatus(lang);
          that.showCommit();
        });
    },

    onchanged: function() {
        this.showCommit();
        this.changed[this.current] = true;
    },

    showCommit: function() {
      if (this.current == this.primary) {
        this.statusbar.remove('translatable:commit');
        return true;
      }

      if (!this.changed[this.current])
        return true;

      var tstatus = $R.dom('<a href="#"></a>')
        .text(__('save translation'))
        .on('click', this.commit.bind(this))
      this.statusbar.add('translatable:commit', tstatus);
    },

    commit: function() {
      if (!this.changed[this.current])
          return this.app.statusbar.remove('translatable:commit');

      var changes = {}, self = this;

      this.app.statusbar.add('translatable:commit', __('saving...'))
      changes[this.current] = this.app.source.getCode();

      $.ajax('ajax.php/i18n/translate/' + this.translateTag, {
        type: 'post',
        data: changes,
        success: function() {
          self.changed[self.current] = false;
          self.app.statusbar.remove('translatable:commit');
        }
      });
      // Don't bubble the click event
      return false;
    },

    urlcache: {},
    fetch: function( url, data, callback ) {
      var urlcache = this.urlcache;
      if ( !urlcache[ url ] ) {
        urlcache[ url ] = $.Deferred(function( defer ) {
          $.ajax( url, { data: data, dataType: 'json' } )
            .then( defer.resolve, defer.reject );
        }).promise();
      }
      return urlcache[ url ].done( callback );
    },
  });
})(Redactor);