/*
  Redactor II
  Version 2.8.1
  Updated: July 4, 2017

  http://imperavi.com/redactor/

  Copyright (c) 2009-2017, Imperavi LLC.
  License: http://imperavi.com/redactor/license/

  Usage: $('#content').redactor();
*/

(function($)
{
  'use strict';

  if (!Function.prototype.bind)
  {
    Function.prototype.bind = function(scope)
    {
      var fn = this;
      return function()
      {
        return fn.apply(scope);
      };
    };
  }

  var uuid = 0;

  // Plugin
  $.fn.redactor = function(options)
  {
    var val = [];
    var args = Array.prototype.slice.call(arguments, 1);

    if (typeof options === 'string')
    {
      this.each(function()
      {
        var instance = $.data(this, 'redactor');
        var func;

        if (options.search(/\./) !== '-1')
        {
          func = options.split('.');
          if (typeof instance[func[0]] !== 'undefined')
          {
            func = instance[func[0]][func[1]];
          }
        }
        else
        {
          func = instance[options];
        }

        if (typeof instance !== 'undefined' && $.isFunction(func))
        {
          var methodVal = func.apply(instance, args);
          if (methodVal !== undefined && methodVal !== instance)
          {
            val.push(methodVal);
          }
        }
        else
        {
          $.error('No such method "' + options + '" for Redactor');
        }
      });
    }
    else
    {
      this.each(function()
      {
        $.data(this, 'redactor', {});
        $.data(this, 'redactor', Redactor(this, options));
      });
    }

    if (val.length === 0)
    {
      return this;
    }
    else if (val.length === 1)
    {
      return val[0];
    }
    else
    {
      return val;
    }

  };

  // Initialization
  function Redactor(el, options)
  {
    return new Redactor.prototype.init(el, options);
  }

  // Options
  $.Redactor = Redactor;
  $.Redactor.VERSION = '2.8.1';
  $.Redactor.modules = ['air', 'autosave', 'block', 'buffer', 'build', 'button', 'caret', 'clean', 'code', 'core', 'detect', 'dropdown',
              'events', 'file', 'focus', 'image', 'indent', 'inline', 'insert', 'keydown', 'keyup',
              'lang', 'line', 'link', 'linkify', 'list', 'marker', 'modal', 'observe', 'offset', 'paragraphize', 'paste', 'placeholder',
              'progress', 'selection', 'shortcuts', 'storage', 'toolbar', 'upload', 'uploads3', 'utils',

              'browser' // deprecated
              ];

  $.Redactor.settings = {};
  $.Redactor.opts = {

    // settings
    animation: false,
    lang: 'en',
    direction: 'ltr',
    spellcheck: true,
    overrideStyles: true,
    scrollTarget: document,

    focus: false,
    focusEnd: false,

    clickToEdit: false,
    structure: false,

    tabindex: false,

    minHeight: false, // string
    maxHeight: false, // string

    maxWidth: false, // string

    plugins: false, // array
    callbacks: {},

    placeholder: false,

    linkify: true,
    enterKey: true,

    pastePlainText: false,
    pasteImages: true,
    pasteLinks: true,
    pasteBlockTags: ['pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'ul', 'ol', 'li', 'blockquote', 'p', 'figure', 'figcaption'],
    pasteInlineTags: ['br', 'strong', 'ins', 'code', 'del', 'span', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small', 'b', 'u', 'em', 'i'],

    preClass: false, // string
    preSpaces: 4, // or false
    tabAsSpaces: false, // true or number of spaces
    tabKey: true,

    autosave: false, // false or url
    autosaveName: false,
    autosaveFields: false,

    imageUpload: null,
    imageUploadParam: 'file',
    imageUploadFields: false,
    imageUploadForms: false,
        imageTag: 'figure',
        imageEditable: true,
    imageCaption: true,

    imagePosition: false,
    imageResizable: false,
    imageFloatMargin: '10px',

    dragImageUpload: true,
    multipleImageUpload: true,
    clipboardImageUpload: true,

    fileUpload: null,
    fileUploadParam: 'file',
    fileUploadFields: false,
    fileUploadForms: false,
    dragFileUpload: true,

    s3: false,

        linkNewTab: false,
    linkTooltip: true,
    linkNofollow: false,
    linkSize: 30,
    pasteLinkTarget: false,

    videoContainerClass: 'video-container',

    toolbar: true,
    toolbarFixed: true,
    toolbarFixedTarget: document,
    toolbarFixedTopOffset: 0, // pixels
    toolbarExternal: false, // ID selector
    toolbarOverflow: false,

    air: false,
    airWidth: false,

    formatting: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
    formattingAdd: false,

    buttons: ['format', 'bold', 'italic', 'deleted', 'lists', 'image', 'file', 'link', 'horizontalrule'], // + 'horizontalrule', 'underline', 'ol', 'ul', 'indent', 'outdent'
        buttonsTextLabeled: false,
    buttonsHide: [],
    buttonsHideOnMobile: [],

    script: true,
    removeNewlines: false,
    removeComments: true,
    replaceTags: {
      'b': 'strong',
      'i': 'em',
      'strike': 'del'
    },

        keepStyleAttr: [], // tag name array

    // shortcuts
    shortcuts: {
      'ctrl+shift+m, meta+shift+m': { func: 'inline.removeFormat' },
      'ctrl+b, meta+b': { func: 'inline.format', params: ['bold'] },
      'ctrl+i, meta+i': { func: 'inline.format', params: ['italic'] },
      'ctrl+h, meta+h': { func: 'inline.format', params: ['superscript'] },
      'ctrl+l, meta+l': { func: 'inline.format', params: ['subscript'] },
      'ctrl+k, meta+k': { func: 'link.show' },
      'ctrl+shift+7':   { func: 'list.toggle', params: ['orderedlist'] },
      'ctrl+shift+8':   { func: 'list.toggle', params: ['unorderedlist'] }
    },
    shortcutsAdd: false,

    activeButtons: ['deleted', 'italic', 'bold'],
    activeButtonsStates: {
      b: 'bold',
      strong: 'bold',
      i: 'italic',
      em: 'italic',
      del: 'deleted',
      strike: 'deleted'
    },

    // private lang
    langs: {
      en: {

        "format": "Format",
        "image": "Image",
        "file": "File",
        "link": "Link",
        "bold": "Bold",
        "italic": "Italic",
        "deleted": "Strikethrough",
        "underline": "Underline",
        "bold-abbr": "B",
        "italic-abbr": "I",
        "deleted-abbr": "S",
        "underline-abbr": "U",
        "lists": "Lists",
        "link-insert": "Insert link",
        "link-edit": "Edit link",
        "link-in-new-tab": "Open link in new tab",
        "unlink": "Unlink",
        "cancel": "Cancel",
        "close": "Close",
        "insert": "Insert",
        "save": "Save",
        "delete": "Delete",
        "text": "Text",
        "edit": "Edit",
        "title": "Title",
        "paragraph": "Normal text",
        "quote": "Quote",
        "code": "Code",
        "heading1": "Heading 1",
        "heading2": "Heading 2",
        "heading3": "Heading 3",
        "heading4": "Heading 4",
        "heading5": "Heading 5",
        "heading6": "Heading 6",
        "filename": "Name",
        "optional": "optional",
        "unorderedlist": "Unordered List",
        "orderedlist": "Ordered List",
        "outdent": "Outdent",
        "indent": "Indent",
        "horizontalrule": "Line",
        "upload-label": "Drop file here or ",
        "caption": "Caption",

        "bulletslist": "Bullets",
        "numberslist": "Numbers",

        "image-position": "Position",
        "none": "None",
        "left": "Left",
        "right": "Right",
        "center": "Center",

        "accessibility-help-label": "Rich text editor"
      }
    },

    // private
    type: 'textarea', // textarea, div, inline, pre
    inline: false,
    inlineTags: ['a', 'span', 'strong', 'strike', 'b', 'u', 'em', 'i', 'code', 'del', 'ins', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small'],
    blockTags: ['pre', 'ul', 'ol', 'li', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',  'dl', 'dt', 'dd', 'div', 'td', 'blockquote', 'output', 'figcaption', 'figure', 'address', 'section', 'header', 'footer', 'aside', 'article', 'iframe'],
    paragraphize: true,
    paragraphizeBlocks: ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption',
              'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea',
              'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p'],
    emptyHtml: '<p>&#x200b;</p>',
    invisibleSpace: '&#x200b;',
    emptyHtmlRendered: $('').html('​').html(),
    imageTypes: ['image/png', 'image/jpeg', 'image/gif'],
    userAgent: navigator.userAgent.toLowerCase(),
    observe: {
      dropdowns: []
    },
    regexps: {
      linkyoutube: /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.\-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig,
      linkvimeo: /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/,
      linkimage: /((https?|www)[^\s]+\.)(jpe?g|png|gif)(\?[^\s-]+)?/ig,
      url: /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/ig
    }

  };

  // Functionality
  Redactor.fn = $.Redactor.prototype = {

    keyCode: {
      BACKSPACE: 8,
      DELETE: 46,
      UP: 38,
      DOWN: 40,
      ENTER: 13,
      SPACE: 32,
      ESC: 27,
      TAB: 9,
      CTRL: 17,
      META: 91,
      SHIFT: 16,
      ALT: 18,
      RIGHT: 39,
      LEFT: 37,
      LEFT_WIN: 91
    },

    // =init
    init: function(el, options)
    {
      this.$element = $(el);
      this.uuid = uuid++;
      this.sBuffer = [];
            this.sRebuffer = [];

      this.loadOptions(options);
      this.loadModules();

      // click to edit
      if (this.opts.clickToEdit && !this.$element.hasClass('redactor-click-to-edit'))
      {
        return this.loadToEdit(options);
      }
      else if (this.$element.hasClass('redactor-click-to-edit'))
      {
        this.$element.removeClass('redactor-click-to-edit');
      }

      // block & inline test tag regexp
      this.reIsBlock = new RegExp('^(' + this.opts.blockTags.join('|' ).toUpperCase() + ')$', 'i');
      this.reIsInline = new RegExp('^(' + this.opts.inlineTags.join('|' ).toUpperCase() + ')$', 'i');

      // set up drag upload
      this.opts.dragImageUpload = (this.opts.imageUpload === null) ? false : this.opts.dragImageUpload;
      this.opts.dragFileUpload = (this.opts.fileUpload === null) ? false : this.opts.dragFileUpload;

      // formatting storage
      this.formatting = {};

      // load lang
      this.lang.load();

      // extend shortcuts
      $.extend(this.opts.shortcuts, this.opts.shortcutsAdd);

      // set editor
      this.$editor = this.$element;

      // detect type of editor
      this.detectType();

      // start callback
      this.core.callback('start');
      this.core.callback('startToEdit');

      // build
      this.start = true;
      this.build.start();

    },
    detectType: function()
    {
      if (this.build.isInline() || this.opts.inline)
      {
        this.opts.type = 'inline';
      }
      else if (this.build.isTag('DIV'))
      {
        this.opts.type = 'div';
      }
      else if (this.build.isTag('PRE'))
      {
        this.opts.type = 'pre';
      }
    },
    loadToEdit: function(options)
    {

      this.$element.on('click.redactor-click-to-edit', $.proxy(function()
      {
        this.initToEdit(options);

      }, this));

      this.$element.addClass('redactor-click-to-edit');

      return;
    },
    initToEdit: function(options)
    {
      $.extend(options.callbacks,  {
        startToEdit: function()
        {
          this.insert.node(this.marker.get(), false);
        },
        initToEdit: function()
        {
          this.selection.restore();
          this.clickToCancelStorage = this.code.get();

          // cancel
          $(this.opts.clickToCancel).off('.redactor-click-to-edit');
          $(this.opts.clickToCancel).show().on('click.redactor-click-to-edit', $.proxy(function(e)
          {
            e.preventDefault();

            this.core.destroy();
            this.events.syncFire = false;
            this.$element.html(this.clickToCancelStorage);
            this.core.callback('cancel', this.clickToCancelStorage);
            this.events.syncFire = true;
            this.clickToCancelStorage = '';
            $(this.opts.clickToCancel).hide();
            $(this.opts.clickToSave).hide();

            this.$element.on('click.redactor-click-to-edit', $.proxy(function()
            {
              this.initToEdit(options);
            }, this));

            this.$element.addClass('redactor-click-to-edit');

          }, this));

          // save
          $(this.opts.clickToSave).off('.redactor-click-to-edit');
          $(this.opts.clickToSave).show().on('click.redactor-click-to-edit', $.proxy(function(e)
          {
            e.preventDefault();

            this.core.destroy();
            this.core.callback('save', this.code.get());
            $(this.opts.clickToCancel).hide();
            $(this.opts.clickToSave).hide();
            this.$element.on('click.redactor-click-to-edit', $.proxy(function()
            {
              this.initToEdit(options);
            }, this));
            this.$element.addClass('redactor-click-to-edit');

          }, this));
        }

      });

      this.$element.redactor(options);
      this.$element.off('.redactor-click-to-edit');

    },
    loadOptions: function(options)
    {
      var settings = {};

      // check namespace
      if (typeof $.Redactor.settings.namespace !== 'undefined')
      {
         if (this.$element.hasClass($.Redactor.settings.namespace))
         {
           settings = $.Redactor.settings;
         }
      }
      else
      {
        settings = $.Redactor.settings;
      }

      this.opts = $.extend(
        {},
        $.Redactor.opts,
        this.$element.data(),
        options
      );

      this.opts = $.extend({}, this.opts, settings);

    },
    getModuleMethods: function(object)
    {
      return Object.getOwnPropertyNames(object).filter(function(property)
      {
        return typeof object[property] === 'function';
      });
    },
    loadModules: function()
    {
      var len = $.Redactor.modules.length;
      for (var i = 0; i < len; i++)
      {
        this.bindModuleMethods($.Redactor.modules[i]);
      }
    },
    bindModuleMethods: function(module)
    {
      if (typeof this[module] === 'undefined')
      {
        return;
      }

      // init module
      this[module] = this[module]();

      var methods = this.getModuleMethods(this[module]);
      var len = methods.length;

      // bind methods
      for (var z = 0; z < len; z++)
      {
        this[module][methods[z]] = this[module][methods[z]].bind(this);
      }
    },

    // =air
    air: function()
    {
      return {
        enabled: false,
        collapsed: function()
        {
          if (this.opts.air)
          {
            this.selection.get().collapseToStart();
          }
        },
        collapsedEnd: function()
        {
          if (this.opts.air)
          {
            this.selection.get().collapseToEnd();
          }
        },
        build: function()
        {
          if (this.detect.isMobile())
          {
            return;
          }

          this.button.hideButtons();
          this.button.hideButtonsOnMobile();

          if (this.opts.buttons.length === 0)
          {
            return;
          }

          this.$air = this.air.createContainer();

          if (this.opts.airWidth !== false)
          {
            this.$air.css('width', this.opts.airWidth);
          }

          this.air.append();
          this.button.$toolbar = this.$air;
          this.button.setFormatting();
          this.button.load(this.$air);

          this.core.editor().on('mouseup.redactor', this, $.proxy(function(e)
          {
            if (this.selection.text() !== '')
            {
              this.air.show(e);
            }
          }, this));

        },
        append: function()
        {
          this.$air.appendTo('body');
        },
        createContainer: function()
        {
          return $('<ul>').addClass('redactor-air').attr({ 'id': 'redactor-air-' + this.uuid, 'role': 'toolbar' }).hide();
        },
        show: function (e)
        {
          //this.marker.remove();
          this.selection.saveInstant();
          //this.selection.restore(false);

          $('.redactor-air').hide();

          var leftFix = 0;
          var width = this.$air.innerWidth();

          if ($(window).width() < (e.clientX + width))
          {
            leftFix = 200;
          }

          this.$air.css({
            left: (e.clientX - leftFix) + 'px',
            top: (e.clientY + 10 + $(document).scrollTop()) + 'px'
          }).show();

          this.air.enabled = true;
          this.air.bindHide();
        },
        bindHide: function()
        {
          $(document).on('mousedown.redactor-air.' + this.uuid, $.proxy(function(e)
          {
            var dropdown = $(e.target).closest('.redactor-dropdown').length;

            if ($(e.target).closest(this.$air).length === 0 && dropdown === 0)
            {
              var hide = this.air.hide(e);
              if (hide !== false)
              {
                this.marker.remove();
              }
            }

          }, this)).on('keydown.redactor-air.' + this.uuid, $.proxy(function(e)
          {
            var key = e.which;
            if ((!this.utils.isRedactorParent(e.target) && !$(e.target).hasClass('redactor-in')) || $(e.target).closest('#redactor-modal').length !== 0)
            {
              return;
            }

            if (key === this.keyCode.ESC)
            {
              this.selection.get().collapseToStart();
              //this.marker.remove();
            }
            else if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE)
            {
              var sel = this.selection.get();
              var range = this.selection.range(sel);
              range.deleteContents();
              //this.marker.remove();
            }
            else if (key === this.keyCode.ENTER)
            {
              this.selection.get().collapseToEnd();
              //this.marker.remove();
            }

            if (this.air.enabled)
            {
              this.air.hide(e);
            }
            else
            {
              this.selection.get().collapseToStart();
              //this.marker.remove();
            }


          }, this));
        },
        hide: function(e)
        {
          var ctrl = e.ctrlKey || e.metaKey || (e.shiftKey && e.altKey);
          if (ctrl)
          {
            return false;
          }

          this.button.setInactiveAll();
          this.$air.fadeOut(100);
          this.air.enabled = false;
          $(document).off('mousedown.redactor-air.' + this.uuid);
          $(document).off('keydown.redactor-air.' + this.uuid);

        }
      };
    },

    // =autosave
    autosave: function()
    {
      return {
        enabled: false,
        html: false,
        init: function()
        {
          if (!this.opts.autosave)
          {
            return;
          }

          this.autosave.enabled = true;
          this.autosave.name = (this.opts.autosaveName) ? this.opts.autosaveName : this.$textarea.attr('name');

        },
        is: function()
        {
          return this.autosave.enabled;
        },
        send: function()
        {
          if (!this.opts.autosave)
          {
            return;
          }

          this.autosave.source = this.code.get();

          if (this.autosave.html === this.autosave.source)
          {
            return;
          }

          // data
          var data = {};
          data.name = this.autosave.name;
          data[this.autosave.name] = this.autosave.source;
          data = this.autosave.getHiddenFields(data);

          // ajax
          var jsxhr = $.ajax({
            url: this.opts.autosave,
            type: 'post',
            data: data
          });

          jsxhr.done(this.autosave.success);
        },
        getHiddenFields: function(data)
        {
          if (this.opts.autosaveFields === false || typeof this.opts.autosaveFields !== 'object')
          {
            return data;
          }

          $.each(this.opts.autosaveFields, $.proxy(function(k, v)
          {
            if (v !== null && v.toString().indexOf('#') === 0)
            {
              v = $(v).val();
            }

            data[k] = v;

          }, this));

          return data;

        },
        success: function(data)
        {
          var json;
          try
          {
            json = JSON.parse(data);
          }
          catch(e)
          {
            //data has already been parsed
            json = data;
          }

          var callbackName = (typeof json.error === 'undefined') ? 'autosave' :  'autosaveError';

          this.core.callback(callbackName, this.autosave.name, json);
          this.autosave.html = this.autosave.source;
        },
        disable: function()
        {
          this.autosave.enabled = false;

          clearInterval(this.autosaveTimeout);
        }
      };
    },

    // =block
    block: function()
    {
      return {
        format: function(tag, attr, value, type)
        {
          tag = (tag === 'quote') ? 'blockquote' : tag;

          this.block.tags = ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'figure'];
          if ($.inArray(tag, this.block.tags) === -1)
          {
            return;
          }

          if (tag === 'p' && typeof attr === 'undefined')
          {
            // remove all
            attr = 'class';
          }

          this.placeholder.hide();
          this.buffer.set();

          return (this.utils.isCollapsed()) ? this.block.formatCollapsed(tag, attr, value, type) : this.block.formatUncollapsed(tag, attr, value, type);
        },
        formatCollapsed: function(tag, attr, value, type)
        {
          this.selection.save();

          var block = this.selection.block();
          var currentTag = block.tagName.toLowerCase();
          if ($.inArray(currentTag, this.block.tags) === -1)
          {
            this.selection.restore();
            return;
          }

                    var clearAllAttrs = false;
          if (currentTag === tag && attr === undefined)
          {
            tag = 'p';
            clearAllAttrs = true;
          }


                    if (clearAllAttrs)
                    {
                        this.block.removeAllClass();
                        this.block.removeAllAttr();
                    }


                    var replaced;
                    if (currentTag === 'blockquote' && this.utils.isEndOfElement(block))
                    {
                        this.marker.remove();

                        replaced = document.createElement('p');
                        replaced.innerHTML = this.opts.invisibleSpace;

                        $(block).after(replaced);
                        this.caret.start(replaced);
                        var $last = $(block).children().last();

                        if ($last.length !== 0 && $last[0].tagName === 'BR')
                        {
                            $last.remove();
                        }
                    }
                    else
                    {
              replaced = this.utils.replaceToTag(block, tag);
                    }


          if (typeof attr === 'object')
          {
            type = value;
            for (var key in attr)
            {
              replaced = this.block.setAttr(replaced, key, attr[key], type);
            }
          }
          else
          {
            replaced = this.block.setAttr(replaced, attr, value, type);
          }


          // trim pre
          if (tag === 'pre' && replaced.length === 1)
          {
            $(replaced).html($.trim($(replaced).html()));
          }

          this.selection.restore();
          this.block.removeInlineTags(replaced);

          return replaced;
        },
        formatUncollapsed: function(tag, attr, value, type)
        {
          this.selection.save();

          var replaced = [];
          var blocks = this.selection.blocks();

                    if (blocks[0] && ($(blocks[0]).hasClass('redactor-in') || $(blocks[0]).hasClass('redactor-box')))
                    {
                        blocks = this.core.editor().find(this.opts.blockTags.join(', '));
          }

          var len = blocks.length;
          for (var i = 0; i < len; i++)
          {
            var currentTag = blocks[i].tagName.toLowerCase();
            if ($.inArray(currentTag, this.block.tags) !== -1 && currentTag !== 'figure')
            {
              var block = this.utils.replaceToTag(blocks[i], tag);

              if (typeof attr === 'object')
              {
                type = value;
                for (var key in attr)
                {
                  block = this.block.setAttr(block, key, attr[key], type);
                }
              }
              else
              {
                block = this.block.setAttr(block, attr, value, type);
              }

              replaced.push(block);
              this.block.removeInlineTags(block);
            }
          }

          this.selection.restore();

          // combine pre
          if (tag === 'pre' && replaced.length !== 0)
          {
            var first = replaced[0];
            $.each(replaced, function(i,s)
            {
              if (i !== 0)
              {
                $(first).append("\n" + $.trim(s.html()));
                $(s).remove();
              }
            });

            replaced = [];
            replaced.push(first);
          }

          return replaced;
        },
        removeInlineTags: function(node)
        {
          node = node[0] || node;

          var tags = this.opts.inlineTags;
          var blocks = ['PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'];

          if ($.inArray(node.tagName, blocks) === - 1)
          {
            return;
          }

          if (node.tagName !== 'PRE')
          {
            var index = tags.indexOf('a');
            tags.splice(index, 1);
          }

          $(node).find(tags.join(',')).not('.redactor-selection-marker').contents().unwrap();
        },
        setAttr: function(block, attr, value, type)
        {
          if (typeof attr === 'undefined')
          {
            return block;
          }

          var func = (typeof type === 'undefined') ? 'replace' : type;

          if (attr === 'class')
          {
            block = this.block[func + 'Class'](value, block);
          }
          else
          {
            if (func === 'remove')
            {
              block = this.block[func + 'Attr'](attr, block);
            }
            else if (func === 'removeAll')
            {
              block = this.block[func + 'Attr'](attr, block);
            }
            else
            {
              block = this.block[func + 'Attr'](attr, value, block);
            }
          }

          return block;

        },
        getBlocks: function(block)
        {
          block = (typeof block === 'undefined') ? this.selection.blocks() : block;

            if ($(block).hasClass('redactor-box'))
            {
                var blocks = [];
                var nodes = this.core.editor().children();
                  $.each(nodes, $.proxy(function(i,node)
              {
                if (this.utils.isBlock(node))
                {
                  blocks.push(node);
                }

              }, this));

              return blocks;
            }

            return block
        },
        replaceClass: function(value, block)
        {
          return $(this.block.getBlocks(block)).removeAttr('class').addClass(value)[0];
        },
        toggleClass: function(value, block)
        {
          return $(this.block.getBlocks(block)).toggleClass(value)[0];
        },
        addClass: function(value, block)
        {
          return $(this.block.getBlocks(block)).addClass(value)[0];
        },
        removeClass: function(value, block)
        {
          return $(this.block.getBlocks(block)).removeClass(value)[0];
        },
        removeAllClass: function(block)
        {
          return $(this.block.getBlocks(block)).removeAttr('class')[0];
        },
        replaceAttr: function(attr, value, block)
        {
          block = this.block.removeAttr(attr, block);

          return $(block).attr(attr, value)[0];
        },
        toggleAttr: function(attr, value, block)
        {
          block = this.block.getBlocks(block);

          var self = this;
          var returned = [];
          $.each(block, function(i,s)
          {
            var $el = $(s);
            if ($el.attr(attr))
            {
              returned.push(self.block.removeAttr(attr, s));
            }
            else
            {
              returned.push(self.block.addAttr(attr, value, s));
            }
          });

          return returned;

        },
        addAttr: function(attr, value, block)
        {
          return $(this.block.getBlocks(block)).attr(attr, value)[0];
        },
        removeAttr: function(attr, block)
        {
          return $(this.block.getBlocks(block)).removeAttr(attr)[0];
        },
        removeAllAttr: function(block)
        {
          block = this.block.getBlocks(block);

          var returned = [];
          $.each(block, function(i,s)
          {
            if (typeof s.attributes === 'undefined')
            {
              returned.push(s);
            }

            var $el = $(s);
            var len = s.attributes.length;
            for (var z = 0; z < len; z++)
            {
              $el.removeAttr(s.attributes[z].name);
            }

            returned.push($el[0]);
          });

          return returned;
        }
      };
    },

    // buffer
    buffer: function()
    {
      return {
        set: function(type)
        {
            if (typeof type === 'undefined')
                    {
                        this.buffer.clear();
                    }

          if (typeof type === 'undefined' || type === 'undo')
          {
            this.buffer.setUndo();
          }
          else
          {
            this.buffer.setRedo();
          }
        },
        setUndo: function()
        {
                    var saved = this.selection.saveInstant();

          var last = this.sBuffer[this.sBuffer.length-1];
          var current = this.core.editor().html();

          var save = (typeof last !== 'undefined' && (last[0] === current)) ? false : true;
            if (save)
            {
            this.sBuffer.push([current, saved]);
          }

          //this.selection.restoreInstant();
        },
        setRedo: function()
        {
          var saved = this.selection.saveInstant();
          this.sRebuffer.push([this.core.editor().html(), saved]);
          //this.selection.restoreInstant();
        },
        add: function()
        {
          this.sBuffer.push([this.core.editor().html(), 0]);
        },
        undo: function()
        {
          if (this.sBuffer.length === 0)
          {
            return;
          }

          var buffer = this.sBuffer.pop();

          this.buffer.set('redo');
          this.core.editor().html(buffer[0]);

          this.selection.restoreInstant(buffer[1]);
          this.observe.load();
        },
        redo: function()
        {
          if (this.sRebuffer.length === 0)
          {
            return;
          }

          var buffer = this.sRebuffer.pop();

          this.buffer.set('undo');
          this.core.editor().html(buffer[0]);

          this.selection.restoreInstant(buffer[1]);
          this.observe.load();
        },
        clear: function()
        {
            this.sRebuffer = [];
        }
      };
    },

    // =build
    build: function()
    {
      return {
        start: function()
        {
          if (this.opts.type === 'inline')
          {
            this.opts.type = 'inline';
          }
          else if (this.opts.type === 'div')
          {
            // empty
            var html = $.trim(this.$editor.html());
            if (html === '')
            {
              this.$editor.html(this.opts.emptyHtml);
            }
          }
          else if (this.opts.type === 'textarea')
          {
            this.build.startTextarea();
          }

          // set in
          this.build.setIn();

          // set id
          this.build.setId();

          // enable
          this.build.enableEditor();

          // options
          this.build.setOptions();

          // call
          this.build.callEditor();


        },
        createContainerBox: function()
        {
          this.$box = $('<div class="redactor-box" role="application" />');
        },
        setIn: function()
        {
          this.core.editor().addClass('redactor-in');
        },
        setId: function()
        {
          var id = (this.opts.type === 'textarea') ? 'redactor-uuid-' + this.uuid : this.$element.attr('id');

          this.core.editor().attr('id', (typeof id === 'undefined') ? 'redactor-uuid-' + this.uuid : id);
        },
        getName: function()
        {
          var name = this.$element.attr('name');

          return (typeof name === 'undefined') ? 'content-' + this.uuid : name;
        },
        loadFromTextarea: function()
        {
          this.$editor = $('<div />');

          // textarea
          this.$textarea = this.$element;
          this.$element.attr('name', this.build.getName());

          // place
          this.$box.insertAfter(this.$element).append(this.$editor).append(this.$element);
                    this.$editor.addClass('redactor-layer');

          if (this.opts.overrideStyles)
          {
              this.$editor.addClass('redactor-styles');
          }

          this.$element.hide();

          this.$box.prepend('<span class="redactor-voice-label" id="redactor-voice-' + this.uuid +'" aria-hidden="false">' + this.lang.get('accessibility-help-label') + '</span>');
          this.$editor.attr({ 'aria-labelledby': 'redactor-voice-' + this.uuid, 'role': 'presentation' });
        },
        startTextarea: function()
        {
          this.build.createContainerBox();

          // load
          this.build.loadFromTextarea();

          // set code
          this.code.start(this.core.textarea().val());

          // set value
          this.core.textarea().val(this.clean.onSync(this.$editor.html()));
        },
        isTag: function(tag)
        {
          return (this.$element[0].tagName === tag);
        },
        isInline: function()
        {
          return (!this.build.isTag('TEXTAREA') && !this.build.isTag('DIV') && !this.build.isTag('PRE'));
        },
        enableEditor: function()
        {
          this.core.editor().attr({ 'contenteditable': true });
        },
        setOptions: function()
        {
          // inline
          if (this.opts.type === 'inline')
          {
            this.opts.enterKey = false;
          }

          // inline & pre
          if (this.opts.type === 'inline' || this.opts.type === 'pre')
          {
            this.opts.toolbarMobile = false;
            this.opts.toolbar = false;
            this.opts.air = false;
            this.opts.linkify = false;

          }

          // spellcheck
          this.core.editor().attr('spellcheck', this.opts.spellcheck);

          // structure
          if (this.opts.structure)
          {
            this.core.editor().addClass('redactor-structure');
          }

          // options sets only in textarea mode
          if (this.opts.type !== 'textarea')
          {
            return;
          }

          // direction
          this.core.box().attr('dir', this.opts.direction);
          this.core.editor().attr('dir', this.opts.direction);

          // tabindex
          if (this.opts.tabindex)
          {
            this.core.editor().attr('tabindex', this.opts.tabindex);
          }

          // min height
          if (this.opts.minHeight)
          {
            this.core.editor().css('min-height', this.opts.minHeight);
          }
          else
          {
            this.core.editor().css('min-height', '40px');
          }

          // max height
          if (this.opts.maxHeight)
          {
            this.core.editor().css('max-height', this.opts.maxHeight);
          }

          // max width
          if (this.opts.maxWidth)
          {
            this.core.editor().css({ 'max-width': this.opts.maxWidth, 'margin': 'auto' });
          }

        },
        callEditor: function()
        {
          this.build.disableBrowsersEditing();

          this.events.init();
          this.build.setHelpers();

          // init buttons
          if (this.opts.toolbar || this.opts.air)
          {
            this.toolbarsButtons = this.button.init();
          }

          // load toolbar
          if (this.opts.air)
          {
            this.air.build();
          }
          else if (this.opts.toolbar)
          {
            this.toolbar.build();
          }

          if (this.detect.isMobile() && this.opts.toolbarMobile && this.opts.air)
          {
            this.opts.toolbar = true;
            this.toolbar.build();
          }

          // observe dropdowns
          if (this.opts.air || this.opts.toolbar)
          {
            this.core.editor().on('mouseup.redactor-observe.' + this.uuid + ' keyup.redactor-observe.' + this.uuid + ' focus.redactor-observe.' + this.uuid + ' touchstart.redactor-observe.' + this.uuid, $.proxy(this.observe.toolbar, this));
            this.core.element().on('blur.callback.redactor', $.proxy(function()
            {
              this.button.setInactiveAll();

            }, this));
          }

          // modal templates init
          this.modal.templates();

          // plugins
          this.build.plugins();

          // autosave
          this.autosave.init();

          // sync code
          this.code.html = this.code.cleaned(this.core.editor().html());

          // init callback
          this.core.callback('init');
          this.core.callback('initToEdit');

          // get images & files list
          this.storage.observe();

          // started
          this.start = false;

        },
        setHelpers: function()
        {
          // linkify
          if (this.opts.linkify)
          {
            this.linkify.format();
          }

          // placeholder
          this.placeholder.init();

          // focus
          if (this.opts.focus)
          {
            setTimeout(this.focus.start, 100);
          }
          else if (this.opts.focusEnd)
          {
            setTimeout(this.focus.end, 100);
          }

        },
        disableBrowsersEditing: function()
        {
          try {
            // FF fix
            document.execCommand('enableObjectResizing', false, false);
            document.execCommand('enableInlineTableEditing', false, false);
            // IE prevent converting links
            document.execCommand("AutoUrlDetect", false, false);
          } catch (e) {}
        },
        plugins: function()
        {
          if (!this.opts.plugins)
          {
            return;
          }

          $.each(this.opts.plugins, $.proxy(function(i, s)
          {
            var func = (typeof RedactorPlugins !== 'undefined' && typeof RedactorPlugins[s] !== 'undefined') ? RedactorPlugins : Redactor.fn;

            if (!$.isFunction(func[s]))
            {
              return;
            }

            this[s] = func[s]();

            // get methods
            var methods = this.getModuleMethods(this[s]);
            var len = methods.length;

            // bind methods
            for (var z = 0; z < len; z++)
            {
              this[s][methods[z]] = this[s][methods[z]].bind(this);
            }

            // append lang
            if (typeof this[s].langs !== 'undefined')
            {
              var lang = {};
              if (typeof this[s].langs[this.opts.lang] !== 'undefined')
              {
                lang = this[s].langs[this.opts.lang];
              }
              else if (typeof this[s].langs[this.opts.lang] === 'undefined' && typeof this[s].langs.en !== 'undefined')
              {
                lang = this[s].langs.en;
              }

              // extend
              var self = this;
              $.each(lang, function(i,s)
              {
                if (typeof self.opts.curLang[i] === 'undefined')
                {
                  self.opts.curLang[i] = s;
                }
              });
            }

            // init
            if ($.isFunction(this[s].init))
            {
              this[s].init();
            }


          }, this));

        }
      };
    },

    // =button
    button: function()
    {
      return {
        toolbar: function()
        {
          return (typeof this.button.$toolbar === 'undefined' || !this.button.$toolbar) ? this.$toolbar : this.button.$toolbar;
        },
        init: function()
        {
          return {
            format:
            {
              title: this.lang.get('format'),
              icon: true,
              dropdown:
              {
                p:
                {
                  title: this.lang.get('paragraph'),
                  func: 'block.format'
                },
                blockquote:
                {
                  title: this.lang.get('quote'),
                  func: 'block.format'
                },
                pre:
                {
                  title: this.lang.get('code'),
                  func: 'block.format'
                },
                h1:
                {
                  title: this.lang.get('heading1'),
                  func: 'block.format'
                },
                h2:
                {
                  title: this.lang.get('heading2'),
                  func: 'block.format'
                },
                h3:
                {
                  title: this.lang.get('heading3'),
                  func: 'block.format'
                },
                h4:
                {
                  title: this.lang.get('heading4'),
                  func: 'block.format'
                },
                h5:
                {
                  title: this.lang.get('heading5'),
                  func: 'block.format'
                },
                h6:
                {
                  title: this.lang.get('heading6'),
                  func: 'block.format'
                }
              }
            },
            bold:
            {
              title: this.lang.get('bold-abbr'),
              icon: true,
              label: this.lang.get('bold'),
              func: 'inline.format'
            },
            italic:
            {
              title: this.lang.get('italic-abbr'),
              icon: true,
              label: this.lang.get('italic'),
              func: 'inline.format'
            },
            deleted:
            {
              title: this.lang.get('deleted-abbr'),
              icon: true,
              label: this.lang.get('deleted'),
              func: 'inline.format'
            },
            underline:
            {
              title: this.lang.get('underline-abbr'),
              icon: true,
              label: this.lang.get('underline'),
              func: 'inline.format'
            },
            lists:
            {
              title: this.lang.get('lists'),
              icon: true,
              dropdown:
              {
                unorderedlist:
                {
                  title: '&bull; ' + this.lang.get('unorderedlist'),
                  func: 'list.toggle'
                },
                orderedlist:
                {
                  title: '1. ' + this.lang.get('orderedlist'),
                  func: 'list.toggle'
                },
                outdent:
                {
                  title: '< ' + this.lang.get('outdent'),
                  func: 'indent.decrease',
                  observe: {
                    element: 'li',
                    out: {
                      attr: {
                        'class': 'redactor-dropdown-link-inactive',
                        'aria-disabled': true
                      }
                    }
                  }
                },
                indent:
                {
                  title: '> ' + this.lang.get('indent'),
                  func: 'indent.increase',
                  observe: {
                    element: 'li',
                    out: {
                      attr: {
                        'class': 'redactor-dropdown-link-inactive',
                        'aria-disabled': true
                      }
                    }
                  }
                }
              }
            },
            ul:
            {
              title: '&bull; ' + this.lang.get('bulletslist'),
              icon: true,
              func: 'list.toggle'
            },
            ol:
            {
              title: '1. ' + this.lang.get('numberslist'),
              icon: true,
              func: 'list.toggle'
            },
            outdent:
            {
              title: this.lang.get('outdent'),
              icon: true,
              func: 'indent.decrease'
            },
            indent:
            {
              title: this.lang.get('indent'),
              icon: true,
              func: 'indent.increase'
            },
            image:
            {
              title: this.lang.get('image'),
              icon: true,
              func: 'image.show'
            },
            file:
            {
              title: this.lang.get('file'),
              icon: true,
              func: 'file.show'
            },
            link:
            {
              title: this.lang.get('link'),
              icon: true,
              dropdown:
              {
                link:
                {
                  title: this.lang.get('link-insert'),
                  func: 'link.show',
                  observe: {
                    element: 'a',
                    'in': {
                      title: this.lang.get('link-edit')
                    },
                    out: {
                      title: this.lang.get('link-insert')
                    }
                  }
                },
                unlink:
                {
                  title: this.lang.get('unlink'),
                  func: 'link.unlink',
                  observe: {
                    element: 'a',
                    out: {
                      attr: {
                        'class': 'redactor-dropdown-link-inactive',
                        'aria-disabled': true
                      }
                    }
                  }
                }
              }
            },
            horizontalrule:
            {
              title: this.lang.get('horizontalrule'),
              icon: true,
              func: 'line.insert'
            }
          };
        },
        setFormatting: function()
        {
          $.each(this.toolbarsButtons.format.dropdown, $.proxy(function (i, s)
          {
            if ($.inArray(i, this.opts.formatting) === -1)
            {
              delete this.toolbarsButtons.format.dropdown[i];
            }

          }, this));

        },
        hideButtons: function()
        {
          if (this.opts.buttonsHide.length !== 0)
          {
            this.button.hideButtonsSlicer(this.opts.buttonsHide);
          }
        },
        hideButtonsOnMobile: function()
        {
          if (this.detect.isMobile() && this.opts.buttonsHideOnMobile.length !== 0)
          {
            this.button.hideButtonsSlicer(this.opts.buttonsHideOnMobile);
          }
        },
        hideButtonsSlicer: function(buttons)
        {
          $.each(buttons, $.proxy(function(i, s)
          {
            var index = this.opts.buttons.indexOf(s);
            if (index !== -1)
            {
                this.opts.buttons.splice(index, 1);
            }

          }, this));
        },
        load: function($toolbar)
        {
          this.button.buttons = [];

          $.each(this.opts.buttons, $.proxy(function(i, btnName)
          {
            if (!this.toolbarsButtons[btnName]
              || (btnName === 'file' && !this.file.is())
              || (btnName === 'image' && !this.image.is()))
            {
              return;
            }

            $toolbar.append($('<li>').append(this.button.build(btnName, this.toolbarsButtons[btnName])));

          }, this));
        },
        buildButtonTooltip: function($btn, title)
        {
            if (this.opts.air || this.detect.isMobile())
            {
                return;
            }

                    var $tooltip = $('<span>');
            $tooltip.addClass('re-button-tooltip');
            $tooltip.html(title);

                    $btn.append($tooltip);
                    $btn.on('mouseover', function()
                    {
                        if ($(this).hasClass('redactor-button-disabled'))
                        {
                            return;
                        }

                        $tooltip.show();
                        $tooltip.css('margin-left', -($tooltip.innerWidth()/2));

                    }).on('mouseout', function()
                    {
                        $tooltip.hide();
                    });
        },
        build: function(btnName, btnObject)
        {
          if (this.opts.toolbar === false)
          {
            return;
          }

                    var title = (typeof btnObject.label !== 'undefined') ? btnObject.label : btnObject.title;
          var $button = $('<a href="javascript:void(null);" alt="' + title + '" rel="' + btnName + '" />')

                    $button.addClass('re-button re-' + btnName);
          $button.attr({ 'role': 'button', 'aria-label': title, 'tabindex': '-1' });

                    if (typeof btnObject.icon !== 'undefined' && !this.opts.buttonsTextLabeled)
                    {
              var $icon = $('<i>');
              $icon.addClass('re-icon-' + btnName);

              $button.append($icon);
              $button.addClass('re-button-icon');

                        this.button.buildButtonTooltip($button, title);
          }
          else
          {
              $button.html(btnObject.title);
          }

          // click
          if (btnObject.func || btnObject.command || btnObject.dropdown)
          {
            this.button.setEvent($button, btnName, btnObject);
          }

          // dropdown
          if (btnObject.dropdown)
          {
            $button.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true);

            var $dropdown = $('<ul class="redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-box-' + btnName + '" style="display: none;">');
            $button.data('dropdown', $dropdown);
            this.dropdown.build(btnName, $dropdown, btnObject.dropdown);
          }

          this.button.buttons.push($button);

          return $button;
        },
        getButtons: function()
        {
          return this.button.toolbar().find('a.re-button');
        },
        getButtonsKeys: function()
        {
          return this.button.buttons;
        },
        setEvent: function($button, btnName, btnObject)
        {
          $button.on('mousedown', $.proxy(function(e)
          {
            e.preventDefault();

            if ($button.hasClass('redactor-button-disabled'))
            {
              return false;
            }

            var type = 'func';
            var callback = btnObject.func;

            if (btnObject.command)
            {
              type = 'command';
              callback = btnObject.command;
            }
            else if (btnObject.dropdown)
            {
              type = 'dropdown';
              callback = false;
            }

            this.button.toggle(e, btnName, type, callback);

            return false;

          }, this));
        },
        toggle: function(e, btnName, type, callback, args)
        {

          if (this.detect.isIe() || !this.detect.isDesktop())
          {
            this.utils.freezeScroll();
            e.returnValue = false;
          }

          if (type === 'command')
          {
            this.inline.format(callback);
          }
          else if (type === 'dropdown')
          {
            this.dropdown.show(e, btnName);
          }
          else
          {
            this.button.clickCallback(e, callback, btnName, args);
          }

          if (type !== 'dropdown')
          {
            this.dropdown.hideAll(false);
          }

          if (this.opts.air && type !== 'dropdown')
          {
            this.air.hide(e);
          }

          if (this.detect.isIe() || !this.detect.isDesktop())
          {
            this.utils.unfreezeScroll();
          }
        },
        clickCallback: function(e, callback, btnName, args)
        {
          var func;

          args = (typeof args === 'undefined') ? btnName : args;

          if ($.isFunction(callback))
          {
            callback.call(this, btnName);
          }
          else if (callback.search(/\./) !== '-1')
          {
            func = callback.split('.');
            if (typeof this[func[0]] === 'undefined')
            {
              return;
            }

            if (typeof args === 'object')
            {
              this[func[0]][func[1]].apply(this, args);
            }
            else
            {
              this[func[0]][func[1]].call(this, args);
            }
          }
          else
          {

            if (typeof args === 'object')
            {
              this[callback].apply(this, args);
            }
            else
            {
              this[callback].call(this, args);
            }
          }

          this.observe.buttons(e, btnName);

        },
        all: function()
        {
          return this.button.buttons;
        },
        get: function(key)
        {
          if (this.opts.toolbar === false)
          {
            return;
          }

          return this.button.toolbar().find('a.re-' + key);
        },
        set: function(key, title)
        {
          if (this.opts.toolbar === false)
          {
            return;
          }

          var $btn = this.button.toolbar().find('a.re-' + key);

          $btn.html(title).attr('aria-label', title);

          return $btn;
        },
        add: function(key, title)
        {
          if (this.button.isAdded(key) !== true)
          {
            return $();
          }

          var btn = this.button.build(key, { title: title });

          this.button.toolbar().append($('<li>').append(btn));

          return btn;
        },
        addFirst: function(key, title)
        {
          if (this.button.isAdded(key) !== true)
          {
            return $();
          }

          var btn = this.button.build(key, { title: title });

          this.button.toolbar().prepend($('<li>').append(btn));

          return btn;
        },
        addAfter: function(afterkey, key, title)
        {
          if (this.button.isAdded(key) !== true)
          {
            return $();
          }

          var btn = this.button.build(key, { title: title });
          var $btn = this.button.get(afterkey);

          if ($btn.length !== 0)
          {
            $btn.parent().after($('<li>').append(btn));
          }
          else
          {
            this.button.toolbar().append($('<li>').append(btn));
          }

          return btn;
        },
        addBefore: function(beforekey, key, title)
        {
          if (this.button.isAdded(key) !== true)
          {
            return $();
          }

          var btn = this.button.build(key, { title: title });
          var $btn = this.button.get(beforekey);

          if ($btn.length !== 0)
          {
            $btn.parent().before($('<li>').append(btn));
          }
          else
          {
            this.button.toolbar().append($('<li>').append(btn));
          }

          return btn;
        },
        isAdded: function(key)
        {
                  var index = this.opts.buttonsHideOnMobile.indexOf(key);
                    if (this.opts.toolbar === false || (index !== -1 && this.detect.isMobile()))
                    {
              return false;
          }

          return true;
        },
        setIcon: function($btn, icon)
        {
            if (!this.opts.buttonsTextLabeled)
            {
              $btn.html(icon).addClass('re-button-icon');
              this.button.buildButtonTooltip($btn, $btn.attr('alt'));
          }
        },
        changeIcon: function(key, newIconClass)
        {
                    var $btn = this.button.get(key);
                    if ($btn.length !== 0)
          {
              $btn.find('i').removeAttr('class').addClass('re-icon-' + newIconClass);
            }
        },
        addCallback: function($btn, callback)
        {
          if (typeof $btn === 'undefined' || this.opts.toolbar === false)
          {
            return;
          }

          var type = (callback === 'dropdown') ? 'dropdown' : 'func';
          var key = $btn.attr('rel');
          $btn.on('mousedown', $.proxy(function(e)
          {
            if ($btn.hasClass('redactor-button-disabled'))
            {
              return false;
            }

            this.button.toggle(e, key, type, callback);

          }, this));
        },
        addDropdown: function($btn, dropdown)
        {
          if (this.opts.toolbar === false)
          {
            return;
          }

          $btn.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true);

          var key = $btn.attr('rel');
          this.button.addCallback($btn, 'dropdown');

          var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-box-' + key + '" style="display: none;">');
          $btn.data('dropdown', $dropdown);

          // build dropdown
          if (dropdown)
          {
            this.dropdown.build(key, $dropdown, dropdown);
          }

          return $dropdown;
        },
        setActive: function(key)
        {
          this.button.get(key).addClass('redactor-act');
        },
        setInactive: function(key)
        {
          this.button.get(key).removeClass('redactor-act');
        },
        setInactiveAll: function(key)
        {
          var $btns = this.button.toolbar().find('a.re-button');

          if (typeof key !== 'undefined')
          {
            $btns = $btns.not('.re-' + key);
          }

          $btns.removeClass('redactor-act');
        },
        disable: function(key)
        {
          this.button.get(key).addClass('redactor-button-disabled');
        },
        enable: function(key)
        {
          this.button.get(key).removeClass('redactor-button-disabled');
        },
        disableAll: function(key)
        {
          var $btns = this.button.toolbar().find('a.re-button');
          if (typeof key !== 'undefined')
          {
            $btns = $btns.not('.re-' + key);
          }

          $btns.addClass('redactor-button-disabled');
        },
        enableAll: function()
        {
          this.button.toolbar().find('a.re-button').removeClass('redactor-button-disabled');
        },
        remove: function(key)
        {
          this.button.get(key).remove();
        }
      };
    },

    // =caret
    caret: function()
    {
      return {
        set: function(node1, node2, end)
        {
                    var cs = this.core.editor().scrollTop();
                    this.core.editor().focus();
                    this.core.editor().scrollTop(cs);

          end = (typeof end === 'undefined') ? 0 : 1;

          node1 = node1[0] || node1;
          node2 = node2[0] || node2;

          var sel = this.selection.get();
          var range = this.selection.range(sel);

          try
          {
            range.setStart(node1, 0);
            range.setEnd(node2, end);
          }
          catch (e) {}

          this.selection.update(sel, range);
        },
        prepare: function(node)
        {
          // firefox focus
          if (this.detect.isFirefox() && typeof this.start !== 'undefined')
          {
            this.core.editor().focus();
          }

          return node[0] || node;
        },
        start: function(node)
        {
          var sel, range;
          node = this.caret.prepare(node);

          if (!node)
          {
            return;
          }

          if (node.tagName === 'BR')
          {
            return this.caret.before(node);
          }

          var $first = $(node).children().first();

          // empty or inline tag
          var inline = this.utils.isInlineTag(node.tagName);
          if (node.innerHTML === '' || inline)
          {
                        this.caret.setStartEmptyOrInline(node, inline);
          }
          // empty inline inside
                    else if ($first && $first.length !== 0 && this.utils.isInlineTag($first[0].tagName) && $first.text() === '')
                    {
                       this.caret.setStartEmptyOrInline($first[0], true);
                    }
                    // block tag
          else
          {
                        sel = window.getSelection();
                        sel.removeAllRanges();

                        range = document.createRange();
                        range.selectNodeContents(node);
                        range.collapse(true);
                        sel.addRange(range);
          }


        },
        setStartEmptyOrInline: function(node, inline)
        {
                    var sel = window.getSelection();
            var range = document.createRange();
          var textNode = document.createTextNode('\u200B');

          range.setStart(node, 0);
          range.insertNode(textNode);
          range.setStartAfter(textNode);
          range.collapse(true);

          sel.removeAllRanges();
          sel.addRange(range);

          // remove invisible text node
          if (!inline)
          {
            this.core.editor().on('keydown.redactor-remove-textnode', function()
            {
              $(textNode).remove();
              $(this).off('keydown.redactor-remove-textnode');
            });
          }
        },
        end: function(node)
        {
          var sel, range;
          node = this.caret.prepare(node);

          if (!node)
          {
            return;
          }

          // empty node
          if (node.tagName !== 'BR' && node.innerHTML === '')
          {
            return this.caret.start(node);
          }

          // br
          if (node.tagName === 'BR')
          {
            var space = document.createElement('span');
            space.className = 'redactor-invisible-space';
            space.innerHTML = '&#x200b;';

            $(node).after(space);

            sel = window.getSelection();
            sel.removeAllRanges();

            range = document.createRange();

            range.setStartBefore(space);
            range.setEndBefore(space);
            sel.addRange(range);

            $(space).replaceWith(function()
            {
              return $(this).contents();
            });

            return;
          }

          if (node.lastChild && node.lastChild.nodeType === 1)
          {
            return this.caret.after(node.lastChild);
          }

                    var sel = window.getSelection();
                    if (sel.getRangeAt || sel.rangeCount)
                    {
                        try {
                            var range = sel.getRangeAt(0);
                            range.selectNodeContents(node);
                            range.collapse(false);

                            sel.removeAllRanges();
                            sel.addRange(range);
                        }
                        catch(e) {}
                    }
        },
        after: function(node)
        {
          var sel, range;
          node = this.caret.prepare(node);


          if (!node)
          {
            return;
          }

          if (node.tagName === 'BR')
          {
            return this.caret.end(node);
          }

          // block tag
          if (this.utils.isBlockTag(node.tagName))
          {
            var next = this.caret.next(node);

            if (typeof next === 'undefined')
            {
              this.caret.end(node);
            }
            else
            {
              // table
              if (next.tagName === 'TABLE')
              {
                next = $(next).find('th, td').first()[0];
              }
              // list
              else if (next.tagName === 'UL' || next.tagName === 'OL')
              {
                next = $(next).find('li').first()[0];
              }

              this.caret.start(next);
            }

            return;
          }

          // inline tag
          var textNode = document.createTextNode('\u200B');

          sel = window.getSelection();
          sel.removeAllRanges();

          range = document.createRange();
          range.setStartAfter(node);
          range.insertNode(textNode);
          range.setStartAfter(textNode);
          range.collapse(true);

          sel.addRange(range);

        },
        before: function(node)
        {
          var sel, range;
          node = this.caret.prepare(node);


          if (!node)
          {
            return;
          }

          // block tag
          if (this.utils.isBlockTag(node.tagName))
          {
            var prev = this.caret.prev(node);

            if (typeof prev === 'undefined')
            {
              this.caret.start(node);
            }
            else
            {
              // table
              if (prev.tagName === 'TABLE')
              {
                prev = $(prev).find('th, td').last()[0];
              }
              // list
              else if (prev.tagName === 'UL' || prev.tagName === 'OL')
              {
                prev = $(prev).find('li').last()[0];
              }

              this.caret.end(prev);
            }

            return;
          }

          // inline tag
          sel = window.getSelection();
          sel.removeAllRanges();

          range = document.createRange();

              range.setStartBefore(node);
              range.collapse(true);

              sel.addRange(range);
        },
        next: function(node)
        {
          var $next = $(node).next();
          if ($next.hasClass('redactor-script-tag, redactor-selection-marker'))
          {
            return $next.next()[0];
          }
          else
          {
            return $next[0];
          }
        },
        prev: function(node)
        {
          var $prev = $(node).prev();
          if ($prev.hasClass('redactor-script-tag, redactor-selection-marker'))
          {
            return $prev.prev()[0];
          }
          else
          {
            return $prev[0];
          }
        },

        // #backward
        offset: function(node)
        {
          return this.offset.get(node);
        }

      };
    },

    // =clean
    clean: function()
    {
      return {
        onSet: function(html)
        {
          html = this.clean.savePreCode(html);
          html = this.clean.saveFormTags(html);

          // convert script tag
          if (this.opts.script)
          {
            html = html.replace(/<script(.*?[^>]?)>([\w\W]*?)<\/script>/gi, '<pre class="redactor-script-tag" $1>$2</pre>');
          }

          // converting entity
          html = html.replace(/\$/g, '&#36;');
          html = html.replace(/&amp;/g, '&');

          // replace special characters in links
          html = html.replace(/<a href="(.*?[^>]?)®(.*?[^>]?)">/gi, '<a href="$1&reg$2">');

          // save markers
          html = html.replace(/<span id="selection-marker-1"(.*?[^>]?)>​<\/span>/gi, '###marker1###');
          html = html.replace(/<span id="selection-marker-2"(.*?[^>]?)>​<\/span>/gi, '###marker2###');

          // replace tags
          var self = this;
          var $div = $("<div/>").html($.parseHTML(html, document, true));

          var replacement = this.opts.replaceTags;
          if (replacement)
          {
                        var keys = Object.keys(this.opts.replaceTags);
              $div.find(keys.join(',')).each(function(i,s)
              {
                self.utils.replaceToTag(s, replacement[s.tagName.toLowerCase()]);
              });
          }

          // add span marker
          $div.find('span, a').attr('data-redactor-span', true);

          // add style cache
          $div.find(this.opts.inlineTags.join(',')).each(function()
          {
              // add style cache
              var $el = $(this);

              if ($el.attr('style'))
              {
                  $el.attr('data-redactor-style-cache', $el.attr('style'));
              }
          });

          html = $div.html();

          // remove tags
          var tags = ['font', 'html', 'head', 'link', 'body', 'meta', 'applet'];
          if (!this.opts.script)
          {
            tags.push('script');
          }

          html = this.clean.stripTags(html, tags);

          // remove html comments
          if (this.opts.removeComments)
          {
              html = html.replace(/<!--[\s\S]*?-->/gi, '');
          }

          // paragraphize
          html = this.paragraphize.load(html);

          // restore markers
          html = html.replace('###marker1###', '<span id="selection-marker-1" class="redactor-selection-marker">​</span>');
          html = html.replace('###marker2###', '<span id="selection-marker-2" class="redactor-selection-marker">​</span>');

          // empty
          if (html.search(/^(||\s||<br\s?\/?>||&nbsp;)$/i) !== -1)
          {
            return this.opts.emptyHtml;
          }

          return html;
        },
        onGet: function(html)
        {
          return this.clean.onSync(html);
        },
        onSync: function(html)
        {
          // remove invisible spaces
          html = html.replace(/\u200B/g, '');
          html = html.replace(/&#x200b;/gi, '');
          //html = html.replace(/&nbsp;&nbsp;/gi, '&nbsp;');

          if (html.search(/^<p>(||\s||<br\s?\/?>||&nbsp;)<\/p>$/i) !== -1)
          {
            return '';
          }

          // remove image resize
          html = html.replace(/<span(.*?)id="redactor-image-box"(.*?[^>])>([\w\W]*?)<img(.*?)><\/span>/gi, '$3<img$4>');
          html = html.replace(/<span(.*?)id="redactor-image-resizer"(.*?[^>])>(.*?)<\/span>/gi, '');
          html = html.replace(/<span(.*?)id="redactor-image-editter"(.*?[^>])>(.*?)<\/span>/gi, '');
          html = html.replace(/<img(.*?)style="(.*?)opacity: 0\.5;(.*?)"(.*?)>/gi, '<img$1style="$2$3"$4>');

          var $div = $("<div/>").html($.parseHTML(html, document, true));

          // remove empty atributes
          $div.find('*[style=""]').removeAttr('style');
          $div.find('*[class=""]').removeAttr('class');
          $div.find('*[rel=""]').removeAttr('rel');
          $div.find('*[data-image=""]').removeAttr('data-image');
          $div.find('*[alt=""]').removeAttr('alt');
          $div.find('*[title=""]').removeAttr('title');
          $div.find('*[data-redactor-style-cache]').removeAttr('data-redactor-style-cache');

          // remove markers
          $div.find('.redactor-invisible-space, .redactor-unlink').each(function()
          {
            $(this).contents().unwrap();
          });

          // remove span without attributes & span marker
            $div.find('span, a').removeAttr('data-redactor-span data-redactor-style-cache').each(function()
          {
              if (this.attributes.length === 0)
              {
                $(this).contents().unwrap();
            }
          });

          // remove rel attribute from img
          $div.find('img').removeAttr('rel');

          $div.find('.redactor-selection-marker, #redactor-insert-marker').remove();

          html = $div.html();

          // reconvert script tag
          if (this.opts.script)
          {
            html = html.replace(/<pre class="redactor-script-tag"(.*?[^>]?)>([\w\W]*?)<\/pre>/gi, '<script$1>$2</script>');
          }

          // restore form tag
          html = this.clean.restoreFormTags(html);

          // remove br in|of li/header tags
          html = html.replace(new RegExp('<br\\s?/?></h', 'gi'), '</h');
          html = html.replace(new RegExp('<br\\s?/?></li>', 'gi'), '</li>');
          html = html.replace(new RegExp('</li><br\\s?/?>', 'gi'), '</li>');

          // pre class
          html = html.replace(/<pre>/gi, "<pre>\n");
          if (this.opts.preClass)
          {
            html = html.replace(/<pre>/gi, '<pre class="' + this.opts.preClass + '">');
          }

          // link nofollow
          if (this.opts.linkNofollow)
          {
            html = html.replace(/<a(.*?)rel="nofollow"(.*?[^>])>/gi, '<a$1$2>');
            html = html.replace(/<a(.*?[^>])>/gi, '<a$1 rel="nofollow">');
          }

          // replace special characters
          var chars = {
            '\u2122': '&trade;',
            '\u00a9': '&copy;',
            '\u2026': '&hellip;',
            '\u2014': '&mdash;',
            '\u2010': '&dash;'
          };

          $.each(chars, function(i,s)
          {
            html = html.replace(new RegExp(i, 'g'), s);
          });

          html = html.replace(/&amp;/g, '&');

          // remove empty paragpraphs
          //html = html.replace(/<p><\/p>/gi, "");

          // remove new lines
                    html = html.replace(/\n{2,}/g, "\n");

                    // remove all newlines
                    if (this.opts.removeNewlines)
                    {
                        html = html.replace(/\r?\n/g, "");
                    }

          return html;
        },
        onPaste: function(html, data, insert)
        {
          // if paste event
          if (insert !== true)
          {
              // remove google docs markers
                        html = html.replace(/<b\sid="internal-source-marker(.*?)">([\w\W]*?)<\/b>/gi, "$2");
              html = html.replace(/<b(.*?)id="docs-internal-guid(.*?)">([\w\W]*?)<\/b>/gi, "$3");

                        // google docs styles
                        html = html.replace(/<span[^>]*(font-style: italic; font-weight: bold|font-weight: bold; font-style: italic)[^>]*>([\w\W]*?)<\/span>/gi, '<b><i>$2</i></b>');
                        html = html.replace(/<span[^>]*(font-style: italic; font-weight: 700|font-weight: 700; font-style: italic)[^>]*>([\w\W]*?)<\/span>/gi, '<b><i>$2</i></b>');
                        html = html.replace(/<span[^>]*font-style: italic[^>]*>([\w\W]*?)<\/span>/gi, '<i>$1</i>');
                        html = html.replace(/<span[^>]*font-weight: bold[^>]*>([\w\W]*?)<\/span>/gi, '<b>$1</b>');
                        html = html.replace(/<span[^>]*font-weight: 700[^>]*>([\w\W]*?)<\/span>/gi, '<b>$1</b>');

                        // op tag
                        html = html.replace(/<o:p[^>]*>/gi, '');
                        html = html.replace(/<\/o:p>/gi, '');

            var msword = this.clean.isHtmlMsWord(html);
            if (msword)
            {
              html = this.clean.cleanMsWord(html);
            }
          }

          html = $.trim(html);

          if (data.pre)
          {
            if (this.opts.preSpaces)
            {
              html = html.replace(/\t/g, new Array(this.opts.preSpaces + 1).join(' '));
            }
          }
          else
          {

            html = this.clean.replaceBrToNl(html);
            html = this.clean.removeTagsInsidePre(html);
          }

          // if paste event
          if (insert !== true)
          {
            html = this.clean.removeEmptyInlineTags(html);

            if (data.encode === false)
            {
              html = html.replace(/&/g, '&amp;');
              html = this.clean.convertTags(html, data);
              html = this.clean.getPlainText(html);
              html = this.clean.reconvertTags(html, data);
            }

          }

          if (data.text)
          {
            html = this.clean.replaceNbspToSpaces(html);
            html = this.clean.getPlainText(html);
          }

          if (data.lists)
          {
              html = html.replace("\n", '<br>');
            }

          if (data.encode)
          {
            html = this.clean.encodeHtml(html);
          }

          if (data.paragraphize)
          {
              // ff bugfix
                        html = html.replace(/ \n/g, ' ');
                        html = html.replace(/\n /g, ' ');

            html = this.paragraphize.load(html);

            // remove empty p
            html = html.replace(/<p><\/p>/g, '');
          }

          // remove paragraphs form lists (google docs bug)
          html = html.replace(/<li><p>/g, '<li>');
          html = html.replace(/<\/p><\/li>/g, '</li>');

          return html;

        },
        getCurrentType: function(html, insert)
        {
          var blocks = this.selection.blocks();

          var data = {
            text: false,
            encode: false,
            paragraphize: true,
            line: this.clean.isHtmlLine(html),
            blocks: this.clean.isHtmlBlocked(html),
            pre: false,
            lists: false,
            block: true,
            inline: true,
            links: true,
            images: true
          };

          if (blocks.length === 1 && this.utils.isCurrentOrParent(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'figcaption']))
          {
            data.text = true;
            data.paragraphize = false;
            data.inline = false;
            data.images = false;
            data.links = false;
            data.line = true;
          }
          else if (this.opts.type === 'inline' || this.opts.enterKey === false)
          {
            data.paragraphize = false;
            data.block = false;
            data.line = true;
          }
          else if (blocks.length === 1 && this.utils.isCurrentOrParent(['li']))
          {
            data.lists = true;
            data.block = false;
            data.paragraphize = false;
            data.images = false;
          }
          else if (blocks.length === 1 && this.utils.isCurrentOrParent(['th', 'td', 'blockquote']))
          {
            data.block = false;
            data.paragraphize = false;

          }
          else if (this.opts.type === 'pre' || (blocks.length === 1 && this.utils.isCurrentOrParent('pre')))
          {
            data.inline = false;
            data.block = false;
            data.encode = true;
            data.pre = true;
            data.paragraphize = false;
            data.images = false;
            data.links = false;
          }

          if (data.line === true)
          {
            data.paragraphize = false;
          }

          if (insert === true)
          {
            data.text = false;
          }

          return data;

        },
        isHtmlBlocked: function(html)
        {
          var match1 = html.match(new RegExp('</(' + this.opts.blockTags.join('|' ).toUpperCase() + ')>', 'gi'));
          var match2 = html.match(new RegExp('<hr(.*?[^>])>', 'gi'));

          return (match1 === null && match2 === null) ? false : true;
        },
        isHtmlLine: function(html)
        {
          if (this.clean.isHtmlBlocked(html))
          {
            return false;
          }

          var matchBR = html.match(/<br\s?\/?>/gi);
          var matchNL = html.match(/\n/gi);

          return (!matchBR && !matchNL) ? true : false;
        },
        isHtmlMsWord: function(html)
        {
          return html.match(/class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i);
        },
        removeEmptyInlineTags: function(html)
        {
          var tags = this.opts.inlineTags;
            var $div = $("<div/>").html($.parseHTML(html, document, true));
                    var self = this;

          var $spans = $div.find('span');
          var $tags = $div.find(tags.join(','));

          $tags.removeAttr('style');

          $tags.each(function()
          {
              var tagHtml = $(this).html();
              if (this.attributes.length === 0 && self.utils.isEmpty(tagHtml))
              {
                  $(this).replaceWith(function()
                  {
                    return $(this).contents();
                  });
              }
          });

                    $spans.each(function()
          {
              var tagHtml = $(this).html();
              if (this.attributes.length === 0)
              {
                  $(this).replaceWith(function()
                  {
                    return $(this).contents();
                  });
              }
          });

          html = $div.html();

          // convert php tags
          html = html.replace('<!--?php', '<?php');
          html = html.replace('<!--?', '<?');
          html = html.replace('?-->', '?>');

          $div.remove();

          return html;
        },
        cleanMsWord: function(html)
        {
            html = html.replace(/<!--[\s\S]*?-->/g, "");
            html = html.replace(/<o:p>[\s\S]*?<\/o:p>/gi, '');
          html = html.replace(/\n/g, " ");
          html = html.replace(/<br\s?\/?>|<\/p>|<\/div>|<\/li>|<\/td>/gi, '\n\n');

          // lists
          var $div = $("<div/>").html(html);

          var lastList = false;
          var lastLevel = 1;
          var listsIds = [];

          $div.find("p[style]").each(function()
          {
            var matches = $(this).attr('style').match(/mso\-list\:l([0-9]+)\slevel([0-9]+)/);

            if (matches)
            {
              var currentList = parseInt(matches[1]);
              var currentLevel = parseInt(matches[2]);
              var listType = $(this).html().match(/^[\w]+\./) ? "ol" : "ul";

              var $li = $("<li/>").html($(this).html());

              $li.html($li.html().replace(/^([\w\.]+)</, '<'));
              $li.find("span:first").remove();

              if (currentLevel == 1 && $.inArray(currentList, listsIds) == -1)
              {
                var $list = $("<" + listType + "/>").attr({"data-level": currentLevel, "data-list": currentList}).html($li);
                $(this).replaceWith($list);

                lastList = currentList;
                listsIds.push(currentList);
              }
              else
              {
                if (currentLevel > lastLevel)
                {
                  var $prevList = $div.find('[data-level="' + lastLevel + '"][data-list="' + lastList + '"]');
                  var $lastList = $prevList;

                  for(var i = lastLevel; i < currentLevel; i++)
                  {
                    $list = $("<" + listType + "/>");
                    $list.appendTo($lastList.find("li").last());

                    $lastList = $list;
                  }

                  $lastList.attr({"data-level": currentLevel, "data-list": currentList}).html($li);

                }
                else
                {
                  var $prevList = $div.find('[data-level="' + currentLevel + '"][data-list="' + currentList + '"]').last();

                  $prevList.append($li);
                }

                lastLevel = currentLevel;
                lastList = currentList;

                $(this).remove();
              }
            }
          });

          $div.find('[data-level][data-list]').removeAttr('data-level data-list');
          html = $div.html();

          return html;
        },
        replaceNbspToSpaces: function(html)
        {
          return html.replace('&nbsp;', ' ');
        },
        replaceBrToNl: function(html)
        {
          return html.replace(/<br\s?\/?>/gi, '\n');
        },
        replaceNlToBr: function(html)
        {
          return html.replace(/\n/g, '<br />');
        },
        convertTags: function(html, data)
        {
                    var $div = $('<div>').html(html);

                    // remove iframe
          $div.find('iframe').remove();

          // link target & attrs
          var $links = $div.find('a');
          $links.removeAttr('style');
          if (this.opts.pasteLinkTarget !== false)
          {
              $links.attr('target', this.opts.pasteLinkTarget);
          }

                    // links
                    if (data.links && this.opts.pasteLinks)
                    {
                      $div.find('a').each(function(i, link)
                      {
                        if (link.href)
                        {
                          var tmp = '##%a href="' + link.href + '"';
                          var attr;
                          for (var j = 0, length = link.attributes.length; j < length; j++)
                          {
                            attr = link.attributes.item(j);
                            if (attr.name !== 'href')
                            {
                              tmp += ' ' + attr.name + '="' + attr.value + '"';
                            }
                          }

                          link.outerHTML = tmp + '%##' + link.innerHTML + '##%/a%##';
                        }
                      });
                    }

                    html = $div.html();

                    // images
          if (data.images && this.opts.pasteImages)
          {
            html = html.replace(/<img(.*?)src="(.*?)"(.*?[^>])>/gi, '##%img$1src="$2"$3%##');
          }

          // plain text
          if (this.opts.pastePlainText)
          {
            return html;
          }

          // all tags
          var blockTags = (data.lists) ? ['ul', 'ol', 'li'] : this.opts.pasteBlockTags;

          var tags;
          if (data.block || data.lists)
          {
            tags = (data.inline) ? blockTags.concat(this.opts.pasteInlineTags) : blockTags;
          }
          else
          {
            tags = (data.inline) ? this.opts.pasteInlineTags : [];
          }

          var len = tags.length;
          for (var i = 0; i < len; i++)
          {
            html = html.replace(new RegExp('<\/' + tags[i] + '>', 'gi'), '###/' + tags[i] + '###');

            if (tags[i] === 'td' || tags[i] === 'th')
            {
              html = html.replace(new RegExp('<' + tags[i] + '(.*?[^>])((colspan|rowspan)="(.*?[^>])")?(.*?[^>])>', 'gi'), '###' + tags[i] + ' $2###');
            }
            else if (this.utils.isInlineTag(tags[i]))
            {
                html = html.replace(new RegExp('<' + tags[i] + '([^>]*)class="([^>]*)"[^>]*>', 'gi'), '###' + tags[i] + ' class="$2"###');
                            html = html.replace(new RegExp('<' + tags[i] + '([^>]*)data-redactor-style-cache="([^>]*)"[^>]*>', 'gi'), '###' + tags[i] + ' cache="$2"###');
                            html = html.replace(new RegExp('<' + tags[i] + '[^>]*>', 'gi'), '###' + tags[i] + '###');
            }
            else
            {
              html = html.replace(new RegExp('<' + tags[i] + '[^>]*>', 'gi'), '###' + tags[i] + '###');
            }
          }

          return html;

        },
        reconvertTags: function(html, data)
        {
          // links & images
          if ((data.links && this.opts.pasteLinks) || (data.images && this.opts.pasteImages))
          {
            html = html.replace(new RegExp('##%', 'gi'), '<');
            html = html.replace(new RegExp('%##', 'gi'), '>');
                    }

          // plain text
          if (this.opts.pastePlainText)
          {
            return html;
          }

          var blockTags = (data.lists) ? ['ul', 'ol', 'li'] : this.opts.pasteBlockTags;

          var tags;
          if (data.block || data.lists)
          {
            tags = (data.inline) ? blockTags.concat(this.opts.pasteInlineTags) : blockTags;
          }
          else
          {
            tags = (data.inline) ? this.opts.pasteInlineTags : [];
          }

          var len = tags.length;
          for (var i = 0; i < len; i++)
          {
            html = html.replace(new RegExp('###\/' + tags[i] + '###', 'gi'), '</' + tags[i] + '>');
                    }

                    for (var i = 0; i < len; i++)
          {
              html = html.replace(new RegExp('###' + tags[i] + '###', 'gi'), '<' + tags[i] + '>');
            }

          for (var i = 0; i < len; i++)
          {
                        if (tags[i] === 'td' || tags[i] === 'th')
                        {
                html = html.replace(new RegExp('###' + tags[i] + '\s?(.*?[^#])###', 'gi'), '<' + tags[i] + '$1>');
                        }
                        else if (this.utils.isInlineTag(tags[i]))
                        {

                            var spanMarker = (tags[i] === 'span') ? ' data-redactor-span="true"' : '';

                            html = html.replace(new RegExp('###' + tags[i] + ' cache="(.*?[^#])"###', 'gi'), '<' + tags[i] + ' style="$1"' + spanMarker + ' data-redactor-style-cache="$1">');
                            html = html.replace(new RegExp('###' + tags[i] + '\s?(.*?[^#])###', 'gi'), '<' + tags[i] + '$1>');
                        }
                    }

          return html;

        },
        cleanPre: function(block)
        {
          block = (typeof block === 'undefined') ? $(this.selection.block()).closest('pre', this.core.editor()[0]) : block;

          $(block).find('br').replaceWith(function()
          {
            return document.createTextNode('\n');
          });

          $(block).find('p').replaceWith(function()
          {
            return $(this).contents();
          });

        },
        removeTagsInsidePre: function(html)
        {
          var $div = $('<div />').append(html);
          $div.find('pre').replaceWith(function()
          {
            var str = $(this).html();
            str = str.replace(/<br\s?\/?>|<\/p>|<\/div>|<\/li>|<\/td>/gi, '\n');
            str = str.replace(/(<([^>]+)>)/gi, '');

            return $('<pre />').append(str);
          });

          html = $div.html();
          $div.remove();

          return html;

        },
        getPlainText: function(html)
        {
          html = html.replace(/<!--[\s\S]*?-->/gi, '');
          html = html.replace(/<style[\s\S]*?style>/gi, '');
                    html = html.replace(/<p><\/p>/g, '');
          html = html.replace(/<\/div>|<\/li>|<\/td>/gi, '\n');
          html = html.replace(/<\/p>/gi, '\n\n');
          html = html.replace(/<\/H[1-6]>/gi, '\n\n');

          var tmp = document.createElement('div');
          tmp.innerHTML = html;
          html = tmp.textContent || tmp.innerText;

          return $.trim(html);
        },
        savePreCode: function(html)
        {
          html = this.clean.savePreFormatting(html);
          html = this.clean.saveCodeFormatting(html);
          html = this.clean.restoreSelectionMarkers(html);

          return html;
        },
        savePreFormatting: function(html)
        {
          var pre = html.match(/<pre(.*?)>([\w\W]*?)<\/pre>/gi);
          if (pre === null)
          {
            return html;
          }

          $.each(pre, $.proxy(function(i,s)
          {
              var arr = [];
              var codeTag = false;
              var contents, attr1, attr2;

              if (s.match(/<pre(.*?)>(([\n\r\s]+)?)<code(.*?)>/i))
              {
                  arr = s.match(/<pre(.*?)>(([\n\r\s]+)?)<code(.*?)>([\w\W]*?)<\/code>(([\n\r\s]+)?)<\/pre>/i);
                  codeTag = true;

                            contents = arr[5];
                            attr1 = arr[1];
                            attr2 = arr[4];
              }
                        else
            {
                arr = s.match(/<pre(.*?)>([\w\W]*?)<\/pre>/i);

                            contents = arr[2];
                            attr1 = arr[1];
                        }

            contents = contents.replace(/<br\s?\/?>/g, '\n');
            contents = contents.replace(/&nbsp;/g, ' ');

            if (this.opts.preSpaces)
            {
              contents = contents.replace(/\t/g, new Array(this.opts.preSpaces + 1).join(' '));
            }

                        contents = this.clean.encodeEntities(contents);

            // $ fix
            contents = contents.replace(/\$/g, '&#36;');

                        if (codeTag)
                        {
                            html = html.replace(s, '<pre' + attr1 + '><code' + attr2 + '>' + contents + '</code></pre>');
                        }
                        else
                        {
                            html = html.replace(s, '<pre' + attr1 + '>' + contents + '</pre>');
                        }


          }, this));

          return html;
        },
        saveCodeFormatting: function(html)
        {
          var code = html.match(/<code(.*?)>([\w\W]*?)<\/code>/gi);
          if (code === null)
          {
            return html;
          }

          $.each(code, $.proxy(function(i,s)
          {
            var arr = s.match(/<code(.*?)>([\w\W]*?)<\/code>/i);

            arr[2] = arr[2].replace(/&nbsp;/g, ' ');
            arr[2] = this.clean.encodeEntities(arr[2]);
            arr[2] = arr[2].replace(/\$/g, '&#36;');

            html = html.replace(s, '<code' + arr[1] + '>' + arr[2] + '</code>');

          }, this));

          return html;
        },
        restoreSelectionMarkers: function(html)
        {
          html = html.replace(/&lt;span id=&quot;selection-marker-([0-9])&quot; class=&quot;redactor-selection-marker&quot;&gt;​&lt;\/span&gt;/g, '<span id="selection-marker-$1" class="redactor-selection-marker">​</span>');

          return html;
        },
        saveFormTags: function(html)
        {
          return html.replace(/<form(.*?)>([\w\W]*?)<\/form>/gi, '<section$1 rel="redactor-form-tag">$2</section>');
        },
        restoreFormTags: function(html)
        {
          return html.replace(/<section(.*?) rel="redactor-form-tag"(.*?)>([\w\W]*?)<\/section>/gi, '<form$1$2>$3</form>');
        },
        encodeHtml: function(html)
        {
          html = html.replace(/”/g, '"');
          html = html.replace(/“/g, '"');
          html = html.replace(/‘/g, '\'');
          html = html.replace(/’/g, '\'');
          html = this.clean.encodeEntities(html);

          return html;
        },
        encodeEntities: function(str)
        {
          str = String(str).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
          str = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

          return str;
        },
        stripTags: function(input, denied)
        {
          if (typeof denied === 'undefined')
          {
            return input.replace(/(<([^>]+)>)/gi, '');
          }

            var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;

            return input.replace(tags, function ($0, $1)
            {
                return denied.indexOf($1.toLowerCase()) === -1 ? $0 : '';
            });
        },
        removeMarkers: function(html)
        {
          return html.replace(/<span(.*?[^>]?)class="redactor-selection-marker"(.*?[^>]?)>([\w\W]*?)<\/span>/gi, '');
        },
        removeSpaces: function(html)
        {
          html = $.trim(html);
          html = html.replace(/\n/g, '');
          html = html.replace(/[\t]*/g, '');
          html = html.replace(/\n\s*\n/g, "\n");
          html = html.replace(/^[\s\n]*/g, ' ');
          html = html.replace(/[\s\n]*$/g, ' ');
          html = html.replace( />\s{2,}</g, '> <'); // between inline tags can be only one space
          html = html.replace(/\n\n/g, "\n");
          html = html.replace(/\u200B/g, '');

          return html;
        },
        removeSpacesHard: function(html)
        {
          html = $.trim(html);
          html = html.replace(/\n/g, '');
          html = html.replace(/[\t]*/g, '');
          html = html.replace(/\n\s*\n/g, "\n");
          html = html.replace(/^[\s\n]*/g, '');
          html = html.replace(/[\s\n]*$/g, '');
          html = html.replace( />\s{2,}</g, '><');
          html = html.replace(/\n\n/g, "\n");
          html = html.replace(/\u200B/g, '');

          return html;
        },
        normalizeCurrentHeading: function()
        {
          var heading = this.selection.block();
          if (this.utils.isCurrentOrParentHeader() && heading)
          {
            heading.normalize();
          }
        }
      };
    },

    // =code
    code: function()
    {
      return {
        syncFire: true,
        html: false,
        start: function(html)
        {
          html = $.trim(html);
          html = html.replace(/^(<span id="selection-marker-1" class="redactor-selection-marker">​<\/span>)/, '');

          // clean
          if (this.opts.type === 'textarea')
          {
            html = this.clean.onSet(html);
          }
          else if (this.opts.type === 'div' && html === '')
          {
            html = this.opts.emptyHtml;
          }

          html = html.replace(/<p><span id="selection-marker-1" class="redactor-selection-marker">​<\/span><\/p>/, '');

          this.events.stopDetectChanges();
          this.core.editor().html(html);
          this.observe.load();
          this.events.startDetectChanges();
        },
        set: function(html, options)
        {
          html = $.trim(html);

                    options = options || {};

                    // start
                    if (options.start)
                    {
                        this.start = options.start;
                    }

          // clean
          if (this.opts.type === 'textarea')
          {
            html = this.clean.onSet(html);
          }
          else if (this.opts.type === 'div' && html === '')
          {
            html = this.opts.emptyHtml;
          }

          this.core.editor().html(html);

          if (this.opts.type === 'textarea')
          {
              this.code.sync();
          }

          this.placeholder.enable();
        },
        get: function()
        {
          if (this.opts.type === 'textarea')
          {
            return this.core.textarea().val();
                    }
          else
          {
            var html = this.core.editor().html();

            // clean
            html = this.clean.onGet(html);

            return html;
          }
        },
        sync: function()
        {
          if (!this.code.syncFire)
          {
            return;
          }

          var html = this.core.editor().html();
          var htmlCleaned = this.code.cleaned(html);

          // is there a need to synchronize
          if (this.code.isSync(htmlCleaned))
          {
            // do not sync
            return;
          }

          // save code
          this.code.html = htmlCleaned;

          if (this.opts.type !== 'textarea')
          {
            this.core.callback('sync', html);
            this.core.callback('change', html);
            return;
          }

          if (this.opts.type === 'textarea')
          {
            setTimeout($.proxy(function()
            {
              this.code.startSync(html);

            }, this), 10);
          }
        },
        startSync: function(html)
        {
          // before clean callback
          html = this.core.callback('syncBefore', html);

          // clean
          html = this.clean.onSync(html);

          // set code
          this.core.textarea().val(html);

          // after sync callback
          this.core.callback('sync', html);

          // change callback
          if (this.start === false)
          {
            this.core.callback('change', html);
          }

          this.start = false;
        },
        isSync: function(htmlCleaned)
        {
          var html = (this.code.html !== false) ? this.code.html : false;

          return (html !== false && html === htmlCleaned);
        },
        cleaned: function(html)
        {
          html = html.replace(/\u200B/g, '');
          return this.clean.removeMarkers(html);
        }
      };
    },

    // =core
    core: function()
    {
      return {

        id: function()
        {
          return this.$editor.attr('id');
        },
        element: function()
        {
          return this.$element;
        },
        editor: function()
        {
          return (typeof this.$editor === 'undefined') ? $() : this.$editor;
        },
        textarea: function()
        {
          return this.$textarea;
        },
        box: function()
        {
          return (this.opts.type === 'textarea') ? this.$box : this.$element;
        },
        toolbar: function()
        {
          return (this.$toolbar) ? this.$toolbar : false;
        },
        air: function()
        {
          return (this.$air) ? this.$air : false;
        },
        object: function()
        {
          return $.extend({}, this);
        },
        structure: function()
        {
          this.core.editor().toggleClass('redactor-structure');
        },
        addEvent: function(name)
        {
          this.core.event = name;
        },
        getEvent: function()
        {
          return this.core.event;
        },
        callback: function(type, e, data)
        {
          var eventNamespace = 'redactor';
          var returnValue = false;
          var events = $._data(this.core.element()[0], 'events');

          // on callback
          if (typeof events !== 'undefined' && typeof events[type] !== 'undefined')
          {
            var len = events[type].length;
            for (var i = 0; i < len; i++)
            {
              var namespace = events[type][i].namespace;
              if (namespace === 'callback.' + eventNamespace)
              {
                var handler = events[type][i].handler;
                var args = (typeof data === 'undefined') ? [e] : [e, data];
                returnValue = (typeof args === 'undefined') ? handler.call(this, e) : handler.call(this, e, args);
              }
            }
          }

          if (returnValue)
          {
            return returnValue;
          }

          // no callback
          if (typeof this.opts.callbacks[type] === 'undefined')
          {
            return (typeof data === 'undefined') ? e : data;
          }

          // callback
          var callback = this.opts.callbacks[type];

          if ($.isFunction(callback))
          {
            return (typeof data === 'undefined') ? callback.call(this, e) : callback.call(this, e, data);
          }
          else
          {
            return (typeof data === 'undefined') ? e : data;
          }
        },
        destroy: function()
        {
          this.opts.destroyed = true;

          this.core.callback('destroy');

          // placeholder
          this.placeholder.destroy();

          // progress
          this.progress.destroy();

          // help label
          $('#redactor-voice-' + this.uuid).remove();

          this.core.editor().removeClass('redactor-in redactor-styles redactor-structure redactor-layer-img-edit');

          // caret service
          this.core.editor().off('keydown.redactor-remove-textnode');

          // observer
          this.core.editor().off('.redactor-observe.' + this.uuid);

          // off events and remove data
          this.$element.off('.redactor').removeData('redactor');
          this.core.editor().off('.redactor');

          $(document).off('.redactor-dropdown');
          $(document).off('.redactor-air.' + this.uuid);
          $(document).off('mousedown.redactor-blur.' + this.uuid);
          $(document).off('mousedown.redactor.' + this.uuid);
          $(document).off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid);
          $(window).off('.redactor-toolbar.' + this.uuid);
          $(window).off('touchmove.redactor.' + this.uuid);
          $("body").off('scroll.redactor.' + this.uuid);

          $(this.opts.toolbarFixedTarget).off('scroll.redactor.' + this.uuid);

          // plugins events
          var self = this;
          if (this.opts.plugins !== false)
          {
            $.each(this.opts.plugins, function(i,s)
            {
              $(window).off('.redactor-plugin-' + s);
              $(document).off('.redactor-plugin-' + s);
              $("body").off('.redactor-plugin-' + s);
              self.core.editor().off('.redactor-plugin-' + s);
            });
          }

          // click to edit
          this.$element.off('click.redactor-click-to-edit');
          this.$element.removeClass('redactor-click-to-edit');

          // common
          this.core.editor().removeClass('redactor-layer');
          this.core.editor().removeAttr('contenteditable');

          var html = this.code.get();

          if (this.opts.toolbar && this.$toolbar)
          {
            // dropdowns off
            this.$toolbar.find('a').each(function()
            {
              var $el = $(this);
              if ($el.data('dropdown'))
              {
                $el.data('dropdown').remove();
                $el.data('dropdown', {});
              }
            });
          }

          if (this.opts.type === 'textarea')
          {
            this.$box.after(this.$element);
            this.$box.remove();
            this.$element.val(html).show();
          }

          // air
          if (this.opts.air)
          {
            this.$air.remove();
          }

          if (this.opts.toolbar && this.$toolbar)
          {
            this.$toolbar.remove();
          }

          // modal
          if (this.$modalBox)
          {
            this.$modalBox.remove();
          }

          if (this.$modalOverlay)
          {
            this.$modalOverlay.remove();
          }

          // hide link's tooltip
          $('.redactor-link-tooltip').remove();

          // autosave
          clearInterval(this.autosaveTimeout);
        }
      };
    },

    // =detect
    detect: function()
    {
      return {

        // public
        isWebkit: function()
        {
          return /webkit/.test(this.opts.userAgent);
        },
        isFirefox: function()
        {
          return this.opts.userAgent.indexOf('firefox') > -1;
        },
        isIe: function(v)
        {
                    if (document.documentMode || /Edge/.test(navigator.userAgent))
                    {
                        return 'edge';
                    }

          var ie;
          ie = RegExp('msie' + (!isNaN(v)?('\\s'+v):''), 'i').test(navigator.userAgent);

          if (!ie)
          {
            ie = !!navigator.userAgent.match(/Trident.*rv[ :]*11\./);
          }

          return ie;
        },
        isMobile: function()
        {
          return /(iPhone|iPod|BlackBerry|Android)/.test(navigator.userAgent);
        },
        isDesktop: function()
        {
          return !/(iPhone|iPod|iPad|BlackBerry|Android)/.test(navigator.userAgent);
        },
        isIpad: function()
        {
          return /iPad/.test(navigator.userAgent);
        }

      };
    },

    // =dropdown
    dropdown: function()
    {
      return {
        active: false,
        button: false,
        key: false,
        position: [],
        getDropdown: function()
        {
          return this.dropdown.active;
        },
        build: function(name, $dropdown, dropdownObject)
        {
          dropdownObject = this.dropdown.buildFormatting(name, dropdownObject);

          $.each(dropdownObject, $.proxy(function(btnName, btnObject)
          {
            var $item = this.dropdown.buildItem(btnName, btnObject);

            this.observe.addDropdown($item, btnName, btnObject);
            $dropdown.attr('rel', name).append($item);

          }, this));
        },
        buildFormatting: function(name, dropdownObject)
        {
          if (name !== 'format' || this.opts.formattingAdd === false)
          {
            return dropdownObject;
          }

          $.each(this.opts.formattingAdd, $.proxy(function(i,s)
          {
            var type = (this.utils.isBlockTag(s.args[0])) ? 'block' : 'inline';

            dropdownObject[i] = {
              func: (type === 'block') ? 'block.format' : 'inline.format',
              args: s.args,
              title: s.title
            };

          }, this));

          return dropdownObject;
        },
        buildItem: function(btnName, btnObject)
        {
          var $itemContainer = $('<li />');
          if (typeof btnObject.classname !== 'undefined')
          {
              $itemContainer.addClass(btnObject.classname);
          }

          if (btnName.search(/^divider/i) !== -1)
          {
              $itemContainer.addClass('redactor-dropdown-divider');

              return $itemContainer;
          }

          var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '" role="button" />');
          var $itemSpan = $('<span />').html(btnObject.title);

          $item.append($itemSpan);
          $item.on('mousedown', $.proxy(function(e)
          {
            e.preventDefault();

            this.dropdown.buildClick(e, btnName, btnObject);

          }, this));

          $itemContainer.append($item);

          return $itemContainer;

        },
        buildClick: function(e, btnName, btnObject)
        {
          if ($(e.target).hasClass('redactor-dropdown-link-inactive'))
          {
            return;
          }

          var command = this.dropdown.buildCommand(btnObject);

          if (typeof btnObject.args !== 'undefined')
          {
            this.button.toggle(e, btnName, command.type, command.callback, btnObject.args);
          }
          else
          {
            this.button.toggle(e, btnName, command.type, command.callback);
          }
        },
        buildCommand: function(btnObject)
        {
          var command = {};
          command.type = 'func';
          command.callback = btnObject.func;

          if (btnObject.command)
          {
            command.type = 'command';
            command.callback = btnObject.command;
          }
          else if (btnObject.dropdown)
          {
            command.type = 'dropdown';
            command.callback = btnObject.dropdown;
          }

          return command;
        },
        show: function(e, key)
        {
          if (this.detect.isDesktop())
          {
            this.core.editor().focus();
          }

          this.dropdown.hideAll(false, key);

          this.dropdown.key = key;
          this.dropdown.button = this.button.get(this.dropdown.key);

          if (this.dropdown.button.hasClass('dropact'))
          {
            this.dropdown.hide();
            return;
          }

          // re append
          this.dropdown.active = this.dropdown.button.data('dropdown').appendTo(document.body);

          // callback
          this.core.callback('dropdownShow', { dropdown: this.dropdown.active, key: this.dropdown.key, button: this.dropdown.button });

          // set button
          this.button.setActive(this.dropdown.key);
          this.dropdown.button.addClass('dropact');

          // position
          this.dropdown.getButtonPosition();

          // show
          if (this.button.toolbar().hasClass('toolbar-fixed-box') && this.detect.isDesktop())
          {
            this.dropdown.showIsFixedToolbar();
          }
          else
          {
            this.dropdown.showIsUnFixedToolbar();
          }

          // disable scroll whan dropdown scroll
          if (this.detect.isDesktop() && !this.detect.isFirefox())
          {
            this.dropdown.active.on('mouseover.redactor-dropdown', $.proxy(this.utils.disableBodyScroll, this));
            this.dropdown.active.on('mouseout.redactor-dropdown mousedown.redactor-dropdown', $.proxy(this.utils.enableBodyScroll, this));
          }

          e.stopPropagation();

        },
        showIsFixedToolbar: function()
        {
          var top = this.dropdown.button.position().top + this.dropdown.button.innerHeight() + this.opts.toolbarFixedTopOffset;

          var position = 'fixed';
          if (this.opts.toolbarFixedTarget !== document)
          {
            top = (this.dropdown.button.innerHeight() + this.$toolbar.offset().top) + this.opts.toolbarFixedTopOffset;
            position = 'absolute';
          }

          this.dropdown.active.css({

            position: position,
            left: this.dropdown.position.left + 'px',
            top: top + 'px'

          }).show();

          // animate
          this.dropdown.active.redactorAnimation('slideDown', { duration: 0.2 }, $.proxy(function()
          {
            this.dropdown.enableCallback();
            this.dropdown.enableEvents();

          }, this));
        },
        showIsUnFixedToolbar: function()
        {
          this.dropdown.active.css({

            position: 'absolute',
            left: this.dropdown.position.left + 'px',
            top: (this.dropdown.button.innerHeight() + this.dropdown.position.top) + 'px'

          }).show();

          // animate
          this.dropdown.active.redactorAnimation(((this.opts.animation) ? 'slideDown' : 'show'), { duration: 0.2 }, $.proxy(function()
          {
            this.dropdown.enableCallback();
            this.dropdown.enableEvents();

          }, this));
        },
        enableEvents: function()
        {
          $(document).on('mousedown.redactor-dropdown', $.proxy(this.dropdown.hideAll, this));
          this.core.editor().on('touchstart.redactor-dropdown', $.proxy(this.dropdown.hideAll, this));
          $(document).on('keyup.redactor-dropdown', $.proxy(this.dropdown.closeHandler, this));
        },
        enableCallback: function()
        {
          this.core.callback('dropdownShown', { dropdown: this.dropdown.active, key: this.dropdown.key, button: this.dropdown.button });
        },
        getButtonPosition: function()
        {
          this.dropdown.position = this.dropdown.button.offset();

          // fix right placement
          var dropdownWidth = this.dropdown.active.width();
          if ((this.dropdown.position.left + dropdownWidth) > $(document).width())
          {
            this.dropdown.position.left = Math.max(0, this.dropdown.position.left - dropdownWidth + parseInt(this.dropdown.button.innerWidth()));
          }

        },
        closeHandler: function(e)
        {
          if (e.which !== this.keyCode.ESC)
          {
            return;
          }

          this.dropdown.hideAll(e);
          this.core.editor().focus();
        },
        hideAll: function(e, key)
        {
          if (this.detect.isDesktop())
          {
            this.utils.enableBodyScroll();
          }

          if (e !== false && $(e.target).closest('.redactor-dropdown').length !== 0)
          {
            return;
          }

          var $buttons = (typeof key === 'undefined') ? this.button.toolbar().find('a.dropact') : this.button.toolbar().find('a.dropact').not('.re-' + key);
          var $elements = (typeof key === 'undefined') ? $('.redactor-dropdown-' + this.uuid) : $('.redactor-dropdown-' + this.uuid).not('.redactor-dropdown-box-' + key);

          if ($elements.length !== 0)
          {
            $(document).off('.redactor-dropdown');
            this.core.editor().off('.redactor-dropdown');

            $.each($elements, $.proxy(function(i,s)
            {
              var $el = $(s);

              this.core.callback('dropdownHide', $el);

              $el.hide();
              $el.off('mouseover mouseout').off('.redactor-dropdown');

            }, this));

            $buttons .removeClass('redactor-act dropact');
          }

        },
        hide: function ()
        {
          if (this.dropdown.active === false)
          {
            return;
          }

          if (this.detect.isDesktop())
          {
            this.utils.enableBodyScroll();
          }

          this.dropdown.active.redactorAnimation(((this.opts.animation) ? 'slideUp' : 'hide'), { duration: 0.2 }, $.proxy(function()
          {
            $(document).off('.redactor-dropdown');
            this.core.editor().off('.redactor-dropdown');

            this.dropdown.hideOut();


          }, this));
        },
        hideOut: function()
        {
          this.core.callback('dropdownHide', this.dropdown.active);

          this.dropdown.button.removeClass('redactor-act dropact');
          this.dropdown.active.off('mouseover mouseout').off('.redactor-dropdown');
          this.dropdown.button = false;
          this.dropdown.key = false;
          this.dropdown.active = false;
        }
      };
    },

    // =events
    events: function()
    {
      return {
        focused: false,
        blured: true,
        dropImage: false,
        stopChanges: false,
        stopDetectChanges: function()
        {
          this.events.stopChanges = true;
        },
        startDetectChanges: function()
        {
          var self = this;
          setTimeout(function()
          {
            self.events.stopChanges = false;
          }, 1);
        },
        dragover: function(e)
        {
          e.preventDefault();
          e.stopPropagation();

          if (e.target.tagName === 'IMG')
          {
            $(e.target).addClass('redactor-image-dragover');
          }

        },
        dragleave: function(e)
        {
          // remove image dragover
          this.core.editor().find('img').removeClass('redactor-image-dragover');
        },
        drop: function(e)
        {
          e = e.originalEvent || e;

          // remove image dragover
          this.core.editor().find('img').removeClass('redactor-image-dragover');

          if (this.opts.type === 'inline' || this.opts.type === 'pre')
          {
            e.preventDefault();
            return false;
          }

          if (window.FormData === undefined || !e.dataTransfer)
          {
            return true;
          }

          if (e.dataTransfer.files.length === 0)
          {
            return this.events.onDrop(e);
          }
          else
          {
            this.events.onDropUpload(e);
          }

          this.core.callback('drop', e);

        },
        click: function(e)
        {
          var event = this.core.getEvent();
          var type = (event === 'click' || event === 'arrow') ? false : 'click';

                    this.core.addEvent(type);
          this.utils.disableSelectAll();
          this.core.callback('click', e);
        },
        focus: function(e)
        {
          if (this.rtePaste)
          {
            return;
          }

          if (this.events.isCallback('focus'))
          {
            this.core.callback('focus', e);
          }

          this.events.focused = true;
          this.events.blured = false;

          // tab
          if (this.selection.current() === false)
          {
            var sel = this.selection.get();
            var range = this.selection.range(sel);

            range.setStart(this.core.editor()[0], 0);
            range.setEnd(this.core.editor()[0], 0);
            this.selection.update(sel, range);
          }

        },
        blur: function(e)
        {
          if (this.start || this.rtePaste)
          {
            return;
          }

          if ($(e.target).closest('#' + this.core.id() + ', .redactor-toolbar, .redactor-dropdown, #redactor-modal-box').length !== 0)
          {
            return;
          }

          if (!this.events.blured && this.events.isCallback('blur'))
          {
            this.core.callback('blur', e);
          }

          this.events.focused = false;
          this.events.blured = true;
        },
        touchImageEditing: function()
        {
          var scrollTimer = -1;
          this.events.imageEditing = false;
          $(window).on('touchmove.redactor.' + this.uuid, $.proxy(function()
          {
            this.events.imageEditing = true;
            if (scrollTimer !== -1)
            {
              clearTimeout(scrollTimer);
            }

            scrollTimer = setTimeout($.proxy(function()
            {
              this.events.imageEditing = false;

            }, this), 500);

          }, this));
        },
        init: function()
        {
          this.core.editor().on('dragover.redactor dragenter.redactor', $.proxy(this.events.dragover, this));
          this.core.editor().on('dragleave.redactor', $.proxy(this.events.dragleave, this));
          this.core.editor().on('drop.redactor', $.proxy(this.events.drop, this));
          this.core.editor().on('click.redactor', $.proxy(this.events.click, this));
          this.core.editor().on('paste.redactor', $.proxy(this.paste.init, this));
          this.core.editor().on('keydown.redactor', $.proxy(this.keydown.init, this));
          this.core.editor().on('keyup.redactor', $.proxy(this.keyup.init, this));
          this.core.editor().on('focus.redactor', $.proxy(this.events.focus, this));

          $(document).on('mousedown.redactor-blur.' + this.uuid, $.proxy(this.events.blur, this));

          this.events.touchImageEditing();

          this.events.createObserver();
          this.events.setupObserver();

        },
        createObserver: function()
        {
          var self = this;
          this.events.observer = new MutationObserver(function(mutations)
          {
            mutations.forEach($.proxy(self.events.iterateObserver, self));
          });

        },
        iterateObserver: function(mutation)
        {

          var stop = false;

          // target
          if (((this.opts.type === 'textarea' || this.opts.type === 'div')
              && (!this.detect.isFirefox() && mutation.target === this.core.editor()[0]))
              || (mutation.attributeName === 'class' && mutation.target === this.core.editor()[0])
                    )
          {
            stop = true;
          }

          if (!stop)
          {
              this.observe.load();
            this.events.changeHandler();
          }
        },
        setupObserver: function()
        {
          this.events.observer.observe(this.core.editor()[0], {
             attributes: true,
             subtree: true,
             childList: true,
             characterData: true,
             characterDataOldValue: true
          });
        },
        changeHandler: function()
        {
          if (this.events.stopChanges)
          {
            return;
          }

          this.code.sync();

          // autosave
          if (this.autosave.is())
          {

            clearTimeout(this.autosaveTimeout);
            this.autosaveTimeout = setTimeout($.proxy(this.autosave.send, this), 300);
          }

        },
        onDropUpload: function(e)
        {
          e.preventDefault();
          e.stopPropagation();

          if ((!this.opts.dragImageUpload && !this.opts.dragFileUpload) || (this.opts.imageUpload === null && this.opts.fileUpload === null))
          {
            return;
          }

          if (e.target.tagName === 'IMG')
          {
            this.events.dropImage = e.target;
          }

          var files = e.dataTransfer.files;
                    var len = files.length;
          for (var i = 0; i < len; i++)
          {
              this.upload.directUpload(files[i], e);
            }
        },
        onDrop: function(e)
        {
          this.core.callback('drop', e);
        },
        isCallback: function(name)
        {
          return (typeof this.opts.callbacks[name] !== 'undefined' && $.isFunction(this.opts.callbacks[name]));
        },

        // #backward
        stopDetect: function()
        {
          this.events.stopDetectChanges();
        },
        startDetect: function()
        {
          this.events.startDetectChanges();
        }

      };
    },

    // =file
    file: function()
    {
      return {
        is: function()
        {
          return !(!this.opts.fileUpload || !this.opts.fileUpload && !this.opts.s3);
        },
        show: function()
        {
          // build modal
          this.modal.load('file', this.lang.get('file'), 700);

          // build upload
          this.upload.init('#redactor-modal-file-upload', this.opts.fileUpload, this.file.insert);

          // set selected text
          $('#redactor-filename').val(this.selection.get().toString());

          // show
          this.modal.show();
        },
        insert: function(json, direct, e)
        {
          // error callback
          if (typeof json.error !== 'undefined')
          {
            this.modal.close();
            this.core.callback('fileUploadError', json);
            return;
          }

          this.file.release(e, direct);

          // prepare
          this.buffer.set();
          this.air.collapsed();

          // get
          var text = this.file.text(json);
          var $link = $('<a />').attr('href', json.url).text(text);
          var id = (typeof json.id === 'undefined') ? '' : json.id;
          var type = (typeof json.s3 === 'undefined') ? 'file' : 's3';

          // set id
          $link.attr('data-' + type, id);

          // insert
          $link = $(this.insert.node($link));

          // focus
          this.caret.after($link);

          // callback
          this.storage.add({ type: type, node: $link[0], url: json.url, id: id });

          if (direct !== null)
          {
            this.core.callback('fileUpload', $link, json);
          }

        },
        release: function(e, direct)
        {
          if (direct)
          {
            // drag and drop upload
            this.marker.remove();
            this.insert.nodeToPoint(e, this.marker.get());
            this.selection.restore();
          }
          else
          {
            // upload from modal
            this.modal.close();
          }
        },
        text: function(json)
        {
          var text = $('#redactor-filename').val();

          return (typeof text === 'undefined' || text === '') ? json.name : text;
        }
      };
    },

    // =focus
    focus: function()
    {
      return {
        start: function()
        {
          this.core.editor().focus();

          if (this.opts.type === 'inline')
          {
            return;
          }

          var $first = this.focus.first();
          if ($first !== false)
          {
            this.caret.start($first);
          }
        },
        end: function()
        {
          this.core.editor().focus();

          var last = (this.opts.inline) ? this.core.editor() : this.focus.last();
          if (last.length === 0)
          {
            return;
          }

          // get inline last node
          var lastNode = this.focus.lastChild(last);
          if (!this.detect.isWebkit() && lastNode !== false)
          {
            this.caret.end(lastNode);
          }
          else
          {
            var sel = this.selection.get();
            var range = this.selection.range(sel);

            if (range !== null)
            {
              range.selectNodeContents(last[0]);
              range.collapse(false);

              this.selection.update(sel, range);
            }
            else
            {
              this.caret.end(last);
            }
          }

        },
        first: function()
        {
          var $first = this.core.editor().children().first();
          if ($first.length === 0 && ($first[0].length === 0 || $first[0].tagName === 'BR' || $first[0].tagName === 'HR' || $first[0].nodeType === 3))
          {
            return false;
          }

          if ($first[0].tagName === 'UL' || $first[0].tagName === 'OL')
          {
            return $first.find('li').first();
          }

          return $first;

        },
        last: function()
        {
          return this.core.editor().children().last();
        },
        lastChild: function(last)
        {
          var lastNode = last[0].lastChild;

          return (lastNode !== null && this.utils.isInlineTag(lastNode.tagName)) ? lastNode : false;
        },
        is: function()
        {
          return (this.core.editor()[0] === document.activeElement);
        }
      };
    },

    // =image
    image: function()
    {
      return {
        is: function()
        {
          return !(!this.opts.imageUpload || !this.opts.imageUpload && !this.opts.s3);
        },
        show: function()
        {
          // build modal
          this.modal.load('image', this.lang.get('image'), 700);

          // build upload
          this.upload.init('#redactor-modal-image-droparea', this.opts.imageUpload, this.image.insert);
          this.modal.show();

        },
        insert: function(json, direct, e)
        {
          var $img;

          // error callback
          if (typeof json.error !== 'undefined')
          {
            this.modal.close();
            this.events.dropImage = false;
            this.core.callback('imageUploadError', json, e);
            return;
          }

          // change image
          if (this.events.dropImage !== false)
          {
            $img = $(this.events.dropImage);

            this.core.callback('imageDelete', $img[0].src, $img);

            $img.attr('src', json.url);

            this.events.dropImage = false;
            this.core.callback('imageUpload', $img, json);
            return;
          }

          this.placeholder.hide();
          var $figure = $('<' + this.opts.imageTag + '>');

          $img = $('<img>');
          $img.attr('src', json.url);

          // set id
          var id = (typeof json.id === 'undefined') ? '' : json.id;
          var type = (typeof json.s3 === 'undefined') ? 'image' : 's3';
          $img.attr('data-' + type, id);

          $figure.append($img);

          var pre = this.utils.isTag(this.selection.current(), 'pre');

          if (direct)
          {
            this.air.collapsed();
            this.marker.remove();

            var node = this.insert.nodeToPoint(e, this.marker.get());
            var $next = $(node).next();

            this.selection.restore();

            // buffer
            this.buffer.set();

            // insert
            if (typeof $next !== 'undefined' && $next.length !== 0 && $next[0].tagName === 'IMG')
            {
              // delete callback
              this.core.callback('imageDelete', $next[0].src, $next);

              // replace
              $next.closest('figure, p', this.core.editor()[0]).replaceWith($figure);
              this.caret.after($figure);
            }
            else
            {
              if (pre)
              {
                $(pre).after($figure);
              }
              else
              {
                this.insert.node($figure);
              }

              this.caret.after($figure);
            }

          }
          else
          {
            this.modal.close();

            // buffer
            this.buffer.set();

            // insert
            this.air.collapsed();

            if (pre)
            {
              $(pre).after($figure);
            }
            else
            {
              this.insert.node($figure);
            }

            this.caret.after($figure);
          }

          this.events.dropImage = false;
          this.storage.add({ type: type, node: $img[0], url: json.url, id: id });

          var nextNode = $img[0].nextSibling;
          var $nextFigure = $figure.next();
          var isNextEmpty = $(nextNode).text().replace(/\u200B/g, '');
          var isNextFigureEmpty = $nextFigure.text().replace(/\u200B/g, '');

          if (isNextEmpty === '')
          {
              $(nextNode).remove();
          }

          if ($nextFigure.length === 1 && $nextFigure[0].tagName === 'FIGURE' && isNextFigureEmpty === '')
          {
              $nextFigure.remove();
          }

          if (direct !== null)
          {
            this.core.callback('imageUpload', $img, json);
          }
          else
          {
              this.core.callback('imageInserted', $img, json);
          }
        },
        setEditable: function($image)
        {
          $image.on('dragstart', function(e)
          {
            e.preventDefault();
          });

                    if (this.opts.imageResizable)
                    {
              var handler = $.proxy(function(e)
              {
                this.observe.image = $image;
                this.image.resizer = this.image.loadEditableControls($image);

                $(document).on('mousedown.redactor-image-resize-hide.' + this.uuid, $.proxy(this.image.hideResize, this));

                if (this.image.resizer)
                {
                                this.image.resizer.on('mousedown.redactor touchstart.redactor', $.proxy(function(e)
                    {
                      this.image.setResizable(e, $image);
                    }, this));
                }

              }, this);

              $image.off('mousedown.redactor').on('mousedown.redactor', $.proxy(this.image.hideResize, this));
              $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor', handler);
                    }
                    else
                    {
              $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor', $.proxy(function(e)
              {
                setTimeout($.proxy(function()
                {
                  this.image.showEdit($image);

                }, this), 200);

              }, this));
                    }

        },
        setResizable: function(e, $image)
        {
          e.preventDefault();

            this.image.resizeHandle = {
                x : e.pageX,
                y : e.pageY,
                el : $image,
                ratio: $image.width() / $image.height(),
                h: $image.height()
            };

            e = e.originalEvent || e;

            if (e.targetTouches)
            {
                 this.image.resizeHandle.x = e.targetTouches[0].pageX;
                 this.image.resizeHandle.y = e.targetTouches[0].pageY;
            }

          this.image.startResize();
        },
        startResize: function()
        {
          $(document).on('mousemove.redactor-image-resize touchmove.redactor-image-resize', $.proxy(this.image.moveResize, this));
          $(document).on('mouseup.redactor-image-resize touchend.redactor-image-resize', $.proxy(this.image.stopResize, this));
        },
        moveResize: function(e)
        {
          e.preventDefault();

          e = e.originalEvent || e;

          var height = this.image.resizeHandle.h;

                if (e.targetTouches) height += (e.targetTouches[0].pageY -  this.image.resizeHandle.y);
                else height += (e.pageY -  this.image.resizeHandle.y);

          var width = Math.round(height * this.image.resizeHandle.ratio);

          if (height < 50 || width < 100) return;

          this.image.resizeHandle.el.attr({width: width, height: height});
                this.image.resizeHandle.el.width(width);
                this.image.resizeHandle.el.height(height);

                this.code.sync();
        },
        stopResize: function()
        {
          this.handle = false;
          $(document).off('.redactor-image-resize');

          this.image.hideResize();
        },
                hideResize: function(e)
        {
          if (e && $(e.target).closest('#redactor-image-box', this.$editor[0]).length !== 0) return;
          if (e && e.target.tagName == 'IMG')
          {
            var $image = $(e.target);
          }

          var imageBox = this.$editor.find('#redactor-image-box');
          if (imageBox.length === 0) return;

          $('#redactor-image-editter').remove();
          $('#redactor-image-resizer').remove();

          imageBox.find('img').css({
            marginTop: imageBox[0].style.marginTop,
            marginBottom: imageBox[0].style.marginBottom,
            marginLeft: imageBox[0].style.marginLeft,
            marginRight: imageBox[0].style.marginRight
          });

          imageBox.css('margin', '');
          imageBox.find('img').css('opacity', '');
          imageBox.replaceWith(function()
          {
            return $(this).contents();
          });

          $(document).off('mousedown.redactor-image-resize-hide.' + this.uuid);

          if (typeof this.image.resizeHandle !== 'undefined')
          {
            this.image.resizeHandle.el.attr('rel', this.image.resizeHandle.el.attr('style'));
          }
        },
        loadResizableControls: function($image, imageBox)
        {
          if (this.opts.imageResizable && !this.detect.isMobile())
          {
            var imageResizer = $('<span id="redactor-image-resizer" data-redactor="verified"></span>');

            if (!this.detect.isDesktop())
            {
              imageResizer.css({ width: '15px', height: '15px' });
            }

            imageResizer.attr('contenteditable', false);
            imageBox.append(imageResizer);
            imageBox.append($image);

            return imageResizer;
          }
          else
          {
            imageBox.append($image);
            return false;
          }
        },
        loadEditableControls: function($image)
        {
            if ($('#redactor-image-box').length !== 0)
            {
                return;
            }

          var imageBox = $('<span id="redactor-image-box" data-redactor="verified">');
          imageBox.css('float', $image.css('float')).attr('contenteditable', false);

          if ($image[0].style.margin != 'auto')
          {
            imageBox.css({
              marginTop: $image[0].style.marginTop,
              marginBottom: $image[0].style.marginBottom,
              marginLeft: $image[0].style.marginLeft,
              marginRight: $image[0].style.marginRight
            });

            $image.css('margin', '');
          }
          else
          {
            imageBox.css({ 'display': 'block', 'margin': 'auto' });
          }

          $image.css('opacity', '.5').after(imageBox);


          if (this.opts.imageEditable)
          {
            // editter
            this.image.editter = $('<span id="redactor-image-editter" data-redactor="verified">' + this.lang.get('edit') + '</span>');
            this.image.editter.attr('contenteditable', false);
            this.image.editter.on('click', $.proxy(function()
            {
              this.image.showEdit($image);
            }, this));

            imageBox.append(this.image.editter);

            // position correction
            var editerWidth = this.image.editter.innerWidth();
            this.image.editter.css('margin-left', '-' + editerWidth/2 + 'px');
          }

          return this.image.loadResizableControls($image, imageBox);

        },
        showEdit: function($image)
        {
          if (this.events.imageEditing)
          {
            return;
          }

          this.observe.image = $image;

          var $link = $image.closest('a', this.$editor[0]);
            var $figure = $image.closest('figure', this.$editor[0]);
            var $container = ($figure.length !== 0) ? $figure : $image;

          this.modal.load('image-edit', this.lang.get('edit'), 705);

          this.image.buttonDelete = this.modal.getDeleteButton().text(this.lang.get('delete'));
          this.image.buttonSave = this.modal.getActionButton().text(this.lang.get('save'));

          this.image.buttonDelete.on('click', $.proxy(this.image.remove, this));
          this.image.buttonSave.on('click', $.proxy(this.image.update, this));

          if (this.opts.imageCaption === false)
          {
            $('#redactor-image-caption').val('').hide().prev().hide();
          }
          else
          {
            var $parent = $image.closest(this.opts.imageTag, this.$editor[0]);
            var $ficaption = $parent.find('figcaption');
            if ($ficaption !== 0)
            {

              $('#redactor-image-caption').val($ficaption.text()).show();
            }
          }

          if (!this.opts.imagePosition)
          {
              $('.redactor-image-position-option').hide();
            }
          else
          {
              var isCentered = ($figure.length !== 0) ? ($container.css('text-align') === 'center') : ($container.css('display') == 'block' && $container.css('float') == 'none');
            var floatValue = (isCentered) ? 'center' : $container.css('float');
            $('#redactor-image-align').val(floatValue);
          }

          $('#redactor-image-preview').html($('<img src="' + $image.attr('src') + '" style="max-width: 100%;">'));
          $('#redactor-image-title').val($image.attr('alt'));

          if ($link.length !== 0)
          {
            $('#redactor-image-link').val($link.attr('href'));
            if ($link.attr('target') === '_blank')
            {
              $('#redactor-image-link-blank').prop('checked', true);
            }
          }

          // hide link's tooltip
          $('.redactor-link-tooltip').remove();

          this.modal.show();

          // focus
          if (this.detect.isDesktop())
          {
            $('#redactor-image-title').focus();
          }

        },
        update: function()
        {
          var $image = this.observe.image;
          var $link = $image.closest('a', this.core.editor()[0]);

          var title = $('#redactor-image-title').val().replace(/(<([^>]+)>)/ig,"");
          $image.attr('alt', title).attr('title', title);

                    this.image.setFloating($image);

          // as link
          var link = $.trim($('#redactor-image-link').val()).replace(/(<([^>]+)>)/ig,"");
          if (link !== '')
          {
            // test url (add protocol)
            var pattern = '((xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}';
            var re = new RegExp('^(http|ftp|https)://' + pattern, 'i');
            var re2 = new RegExp('^' + pattern, 'i');

            if (link.search(re) === -1 && link.search(re2) === 0 && this.opts.linkProtocol)
            {
              link = this.opts.linkProtocol + '://' + link;
            }

            var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false;

            if ($link.length === 0)
            {
              var a = $('<a href="' + link + '" id="redactor-img-tmp">' + this.utils.getOuterHtml($image) + '</a>');
              if (target)
              {
                a.attr('target', '_blank');
              }

              $image = $image.replaceWith(a);
              $link = this.core.editor().find('#redactor-img-tmp');
              $link.removeAttr('id');
            }
            else
            {
              $link.attr('href', link);
              if (target)
              {
                $link.attr('target', '_blank');
              }
              else
              {
                $link.removeAttr('target');
              }
            }
          }
          else if ($link.length !== 0)
          {
            $link.replaceWith(this.utils.getOuterHtml($image));
          }

                    this.image.addCaption($image, $link);
          this.modal.close();

          // buffer
          this.buffer.set();

        },
        setFloating: function($image)
        {
            var $figure = $image.closest('figure', this.$editor[0]);
            var $container = ($figure.length !== 0) ? $figure : $image;
          var floating = $('#redactor-image-align').val();

          var imageFloat = '';
          var imageDisplay = '';
          var imageMargin = '';
          var textAlign = '';

          switch (floating)
          {
            case 'left':
              imageFloat = 'left';
              imageMargin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0';
            break;
            case 'right':
              imageFloat = 'right';
              imageMargin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin;
            break;
            case 'center':

                            if ($figure.length !== 0)
                            {
                                textAlign = 'center';
                            }
                            else
                            {
                  imageDisplay = 'block';
                  imageMargin = 'auto';
              }

            break;
          }

          $container.css({ 'float': imageFloat, 'display': imageDisplay, 'margin': imageMargin, 'text-align': textAlign });
          $container.attr('rel', $image.attr('style'));
        },
        addCaption: function($image, $link)
        {
                    var caption = $('#redactor-image-caption').val();

                    var $target = ($link.length !== 0) ? $link : $image;
          var $figcaption = $target.next();

          if ($figcaption.length === 0 || $figcaption[0].tagName !== 'FIGCAPTION')
          {
            $figcaption = false;
          }

          if (caption !== '')
          {
            if ($figcaption === false)
            {
              $figcaption = $('<figcaption />').text(caption);
              $target.after($figcaption);
            }
            else
            {
              $figcaption.text(caption);
            }
          }
          else if ($figcaption !== false)
          {
            $figcaption.remove();
          }
        },
        remove: function(e, $image, index)
        {
          $image = (typeof $image === 'undefined') ? $(this.observe.image) : $image;

          // delete from modal
          if (typeof e !== 'boolean')
          {
            this.buffer.set();
          }

          this.events.stopDetectChanges();

          var $link = $image.closest('a', this.core.editor()[0]);
          var $figure = $image.closest(this.opts.imageTag, this.core.editor()[0]);
          var $parent = $image.parent();

          // callback
                    var imageDeleteStop = this.core.callback('imageDelete', e, $image[0]);
          if (imageDeleteStop === false)
          {
            if (e) e.preventDefault();
            return false;
          }

          if ($('#redactor-image-box').length !== 0)
          {
            $parent = $('#redactor-image-box').parent();
          }

          var $next, $prev;
          if ($figure.length !== 0)
          {
            $prev = $figure.prev();
            $next = $figure.next();
            $figure.remove();
          }
          else if ($link.length !== 0)
          {
            $parent = $link.parent();
            $link.remove();
          }
          else
          {
            $image.remove();
          }

          $('#redactor-image-box').remove();

          if (e !== false)
          {
            if ($next && $next.length !== 0)
            {
              this.caret.start($next);
            }
            else if ($prev && $prev.length !== 0)
            {
                            this.caret.end($prev);
            }
          }

          if (typeof e !== 'boolean')
          {
            this.modal.close();
          }

          this.utils.restoreScroll();
          this.observe.image = false;
          this.events.startDetectChanges();
          this.placeholder.enable();
          this.code.sync();

        }
      };
    },

    // =indent
    indent: function()
    {
      return {
        increase: function()
        {
          if (!this.list.get())
          {
            return;
          }

          var $current = $(this.selection.current()).closest('li');
          var $list = $current.closest('ul, ol', this.core.editor()[0]);

          var $li = $current.closest('li');
          var $prev = $li.prev();
          if ($prev.length === 0 || $prev[0].tagName !== 'LI')
          {
            return;
          }

          this.buffer.set();


          if (this.utils.isCollapsed())
          {
            var listTag = $list[0].tagName;
            var $newList = $('<' + listTag + ' />');

            this.selection.save();

                        var $ol = $prev.find('ol').first();
                        if ($ol.length === 1)
                        {
                            $ol.append($current);
                        }
                        else
                        {
                            var listTag = $list[0].tagName;
                            var $newList = $('<' + listTag + ' />');
                            $newList.append($current);
                            $prev.append($newList);
                        }

            this.selection.restore();
          }
          else
          {
            document.execCommand('indent');

            // normalize
            this.selection.save();
            this.indent.removeEmpty();
            this.indent.normalize();
            this.selection.restore();
          }
        },
        decrease: function()
        {
          if (!this.list.get())
          {
            return;
          }

          var $current = $(this.selection.current()).closest('li');
          var $list = $current.closest('ul, ol', this.core.editor()[0]);

          this.buffer.set();

          document.execCommand('outdent');

          var $item = $(this.selection.current()).closest('li', this.core.editor()[0]);

          if (this.utils.isCollapsed())
          {
            this.indent.repositionItem($item);
          }

          if ($item.length === 0)
          {
            document.execCommand('formatblock', false, 'p');
            $item = $(this.selection.current());
            var $next = $item.next();
            if ($next.length !== 0 && $next[0].tagName === 'BR')
            {
              $next.remove();
            }
          }

          // normalize
          this.selection.save();
          this.indent.removeEmpty();
          this.indent.normalize();
          this.selection.restore();

        },
        repositionItem: function($item)
        {
            var $next = $item.next();
            if ($next.length !== 0 && ($next[0].tagName !== 'UL' || $next[0].tagName !== 'OL'))
            {
                $item.append($next);
            }

          var $prev = $item.prev();
          if ($prev.length !== 0 && $prev[0].tagName !== 'LI')
          {
            this.selection.save();
            var $li = $item.parents('li', this.core.editor()[0]);
            $li.after($item);
            this.selection.restore();
          }
        },
        normalize: function()
        {
          this.core.editor().find('li').each($.proxy(function(i,s)
          {
            var $el = $(s);

            // remove style
            $el.find(this.opts.inlineTags.join(',')).each(function()
            {
              $(this).removeAttr('style');
            });

            var $parent = $el.parent();
            if ($parent.length !== 0 && $parent[0].tagName === 'LI')
            {
              $parent.after($el);
              return;
            }

            var $next = $el.next();
            if ($next.length !== 0 && ($next[0].tagName === 'UL' || $next[0].tagName === 'OL'))
            {
              $el.append($next);
            }

          }, this));

        },
        removeEmpty: function($list)
        {
          var $lists = this.core.editor().find('ul, ol');
          var $items = this.core.editor().find('li');

          $items.each($.proxy(function(i, s)
          {
            this.indent.removeItemEmpty(s);

          }, this));

          $lists.each($.proxy(function(i, s)
          {
            this.indent.removeItemEmpty(s);

          }, this));

          $items.each($.proxy(function(i, s)
          {
            this.indent.removeItemEmpty(s);

          }, this));
        },
        removeItemEmpty: function(s)
        {
          var html = s.innerHTML.replace(/[\t\s\n]/g, '');
          html = html.replace(/<span><\/span>/g, '');

          if (html === '')
          {
            $(s).remove();
          }
        }
      };
    },

    // =inline
    inline: function()
    {
      return {
                format: function(tag, attr, value, type)
        {
            // Stop formatting pre/code
          if (this.utils.isCurrentOrParent(['PRE', 'CODE'])) return;

          // Get params
          var params = this.inline.getParams(attr, value, type);

                    // Arrange tag
                    tag = this.inline.arrangeTag(tag);

                    this.placeholder.hide();
          this.buffer.set();

          (this.utils.isCollapsed()) ? this.inline.formatCollapsed(tag, params) : this.inline.formatUncollapsed(tag, params);
        },
        formatCollapsed: function(tag, params)
        {
            var newInline;
            var inline = this.selection.inline();

            if (inline)
          {
                        var currentTag = inline.tagName.toLowerCase();
                        if (currentTag === tag)
            {
              // empty = remove
              if (this.utils.isEmpty(inline.innerHTML))
              {
                this.caret.after(inline);
                $(inline).remove();
              }
              // not empty = break
              else
              {
                var $first = this.inline.insertBreakpoint(inline, currentTag);
                this.caret.after($first);
              }
            }
                        else if ($(inline).closest(tag).length === 0)
                        {
              newInline = this.inline.insertInline(tag);
              newInline = this.inline.setParams(newInline, params);
            }
            else
            {
                var $first = this.inline.insertBreakpoint(inline, currentTag);
                            this.caret.after($first);
            }
            }
            else
            {
                        newInline = this.inline.insertInline(tag);
                        newInline = this.inline.setParams(newInline, params);
            }
        },
        formatUncollapsed: function(tag, params)
        {
            this.selection.save();

                    var nodes = this.inline.getClearedNodes();
                    this.inline.setNodesStriked(nodes, tag, params);

                    this.selection.restore();

                    document.execCommand('strikethrough');

                    this.selection.saveInstant();

                    var self = this;
                    this.core.editor().find('strike').each(function()
            {
                        var $el = self.utils.replaceToTag(this, tag);
                self.inline.setParams($el[0], params);

                var $inside = $el.find(tag);
                var $parent = $el.parent();
                var $parentAround = $parent.parent();

                        // revert formatting (safari bug)
                        if ($parentAround.length !== 0 && $parentAround[0].tagName.toLowerCase() === tag && $parentAround.html() == $parent[0].outerHTML)
                        {
                            $el.replaceWith(function() { return $(this).contents(); });
                            $parentAround.replaceWith(function() { return $(this).contents(); });

                            return;
                        }

                // remove inside
                if ($inside.length !== 0) self.inline.cleanInsideOrParent($inside, params);

                        // same parent
                        if ($parent.html() == $el[0].outerHTML) self.inline.cleanInsideOrParent($parent, params);

                        // bugfix: remove empty inline tags after selection
                        if (self.detect.isFirefox())
                        {
                            self.core.editor().find(tag + ':empty').remove();
                        }
            });

            this.selection.restoreInstant();
        },
        cleanInsideOrParent: function($el, params)
        {
                    if (params)
                    {
                        for (var key in params.data)
                        {
                    this.inline.removeSpecificAttr($el, key, params.data[key]);
                }
            }
        },
          getClearedNodes: function()
          {
                    var nodes = this.selection.nodes();
              var newNodes = [];
              var len = nodes.length;
                    var started = 0;

                    // find array slice
                    for (var i = 0; i < len; i++)
          {
            if ($(nodes[i]).hasClass('redactor-selection-marker'))
            {
                started = i + 2;
                break;
            }
          }

                    // find selected inline & text nodes
                    for (var i = 0; i < len; i++)
          {
            if (i >= started && !this.utils.isBlockTag(nodes[i].tagName))
            {
                            newNodes.push(nodes[i]);
            }
          }

          return newNodes;
          },
                isConvertableAttr: function(node, name, value)
                {
                    var nodeAttrValue = $(node).attr(name);
                    if (nodeAttrValue)
                    {
                        if (name === 'style')
                        {
                            value = $.trim(value).replace(/;$/, '')

                            var rules = value.split(';');
                            var count = 0;
                            for (var i = 0; i < rules.length; i++)
                            {
                                var arr = rules[i].split(':');
                                var ruleName = $.trim(arr[0]);
                                var ruleValue = $.trim(arr[1]);

                                if (ruleName.search(/color/) !== -1)
                                {
                                    var val = $(node).css(ruleName);
                                    if (val && (val === ruleValue || this.utils.rgb2hex(val) === ruleValue))
                                    {
                                        count++;
                                    }
                                }
                                else if ($(node).css(ruleName) === ruleValue)
                                {
                                    count++;
                                }
                            }

                            if (count === rules.length)
                            {
                                return 1;
                            }
                        }
                        else if (nodeAttrValue === value)
                        {
                            return 1;
                        }
                    }

                    return 0;

                },
          isConvertable: function(node, nodeTag, tag, params)
          {
                    if (nodeTag === tag)
                    {
                        if (params)
                        {
                            var count = 0;
                            for (var key in params.data)
                            {
                                count += this.inline.isConvertableAttr(node, key, params.data[key]);
                            }

                            if (count === Object.keys(params.data).length)
                            {
                                return true;
                            }
                        }
                        else
                        {
                            return true;
                        }
                    }

                    return false;
          },
          setNodesStriked: function(nodes, tag, params)
          {
                    for (var i = 0; i < nodes.length; i++)
          {
                        var nodeTag = (nodes[i].tagName) ? nodes[i].tagName.toLowerCase() : undefined;

                        var parent = nodes[i].parentNode;
                        var parentTag = (parent && parent.tagName) ? parent.tagName.toLowerCase() : undefined;

                        var convertable = this.inline.isConvertable(parent, parentTag, tag, params);
                        if (convertable)
                        {
                            var $el = $(parent).replaceWith(function()
                            {
                                return $('<strike>').append($(this).contents());
                            });

                            $el.attr('data-redactor-inline-converted');
                        }

                        var convertable = this.inline.isConvertable(nodes[i], nodeTag, tag, params);
                        if (convertable)
                        {
                            var $el = $(nodes[i]).replaceWith(function()
                            {
                                return $('<strike>').append($(this).contents());
                            });
                        }
            }
          },
                insertBreakpoint: function(inline, currentTag)
        {
          var breakpoint = document.createElement('span');
          breakpoint.id = 'redactor-inline-breakpoint';
          breakpoint = this.insert.node(breakpoint);

          var end = this.utils.isEndOfElement(inline);
          var code = this.utils.getOuterHtml(inline);
          var endTag = (end) ? '' : '<' + currentTag + '>';

                    code = code.replace(/<span id="redactor-inline-breakpoint"><\/span>/i, "</" + currentTag + ">" + endTag);

          var $code = $(code);
          $(inline).replaceWith($code);

          if (endTag !== '')
          {
              this.utils.cloneAttributes(inline, $code.last());
          }

          return $code.first();
        },
                insertInline: function(tag)
        {
          var node = document.createElement(tag);

          this.insert.node(node);
          this.caret.start(node);

          return node;
        },
        arrangeTag: function(tag)
        {
                    var tags = ['b', 'bold', 'i', 'italic', 'underline', 'strikethrough', 'deleted', 'superscript', 'subscript'];
          var replaced = ['strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub'];

            tag = tag.toLowerCase();

          for (var i = 0; i < tags.length; i++)
          {
            if (tag === tags[i])
            {
              tag = replaced[i];
            }
          }

          return tag;
        },
        getStyleParams: function(params)
        {
                    var result = {};
            var rules = params.trim().replace(/;$/, '').split(';');
            for (var i = 0; i < rules.length; i++)
            {
                var rule = rules[i].split(':');
                if (rule)
                {
                            result[rule[0].trim()] = rule[1].trim();
                        }
            }

            return result;
        },
        getParams: function(attr, value, type)
        {
            var data = false;
            var func = 'toggle';
            if (typeof attr === 'object')
            {
                data = attr;
                func = (value !== undefined) ? value : func;
            }
            else if (attr !== undefined && value !== undefined)
            {
                data = {};
                data[attr] = value;
                func = (type !== undefined) ? type : func;
            }

            return (data) ? { 'func': func, 'data': data } : false;
        },
        setParams: function(node, params)
        {
                    if (params)
                    {
                        for (var key in params.data)
                        {
                            var $node = $(node);
                            if (key === 'style')
                            {
                                node = this.inline[params.func + 'Style'](params.data[key], node);
                                $node.attr('data-redactor-style-cache', $node.attr('style'));
                            }
                            else if (key === 'class')
                            {
                                node = this.inline[params.func + 'Class'](params.data[key], node);
                            }
                            // attr
                            else
                            {
                                node = (params.func === 'remove') ? this.inline[params.func + 'Attr'](key, node) : this.inline[params.func + 'Attr'](key, params.data[key], node);
                            }

                            if (key === 'style' && node.tagName === 'SPAN')
                            {
                                $node.attr('data-redactor-span', true);
                            }
                        }
                    }

                    return node;
        },

                // Each
                eachInline: function(node, callback)
                {
                    var lastNode;
                    var nodes = (node === undefined) ? this.selection.inlines() : [node];
            if (nodes)
            {
                for (var i = 0; i < nodes.length; i++)
                {
                    lastNode = callback(nodes[i])[0];
                }
            }

            return lastNode;
                },

                // Class
                replaceClass: function(value, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).removeAttr('class').addClass(value);
            });
        },
        toggleClass: function(value, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).toggleClass(value);
            });
        },
        addClass: function(value, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).addClass(value);
            });
        },
        removeClass: function(value, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).removeClass(value);
            });
        },
        removeAllClass: function(node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).removeAttr('class');
            });
        },

        // Attr
        replaceAttr: function(name, value, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).removeAttr(name).attr(name. value);
            });
        },
        toggleAttr: function(name, value, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                var attr = $(el).attr(name);

                return (attr) ? $(el).removeAttr(name) : $(el).attr(name. value);
            });
        },
        addAttr: function(name, value, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).attr(name, value);
            });
        },
        removeAttr: function(name, node)
        {
            return this.inline.eachInline(node, function(el)
            {
                var $el = $(el);

                        $el.removeAttr(name);
                        if (name === 'style')
                        {
                    $el.removeAttr('data-redactor-style-cache');
                }

                        return $el;
            });
        },
        removeAllAttr: function(node)
        {
            return this.inline.eachInline(node, function(el)
            {
                var $el = $(el);
                        var len = el.attributes.length;
            for (var z = 0; z < len; z++)
            {
              $el.removeAttr(el.attributes[z].name);
            }

                        return $el;
            });
        },
        removeSpecificAttr: function(node, key, value)
        {
            var $el = $(node);
            if (key === 'style')
            {
                var arr = value.split(':');
                var name = arr[0].trim();
                        $el.css(name, '');

                        if (this.utils.removeEmptyAttr(node, 'style'))
                        {
                            $el.removeAttr('data-redactor-style-cache');
                        }
            }
            else
            {
                $el.removeAttr(key)[0];
            }
        },

        // Style
        hasParentStyle: function($el)
        {
                    var $parent = $el.parent();

                    return ($parent.length === 1 && $parent[0].tagName === $el[0].tagName && $parent.html() === $el[0].outerHTML) ? $parent : false;
        },
        addParentStyle: function($el)
        {
                    var $parent = this.inline.hasParentStyle($el);
                    if ($parent)
                    {
                        var style = this.inline.getStyleParams($el.attr('style'));
                        $parent.css(style);
                        $parent.attr('data-redactor-style-cache', $parent.attr('style'));

                        $el.replaceWith(function()
                        {
                            return $(this).contents();
                        });
                    }
                    else
                    {
                        $el.attr('data-redactor-style-cache', $el.attr('style'));
                    }

                    return $el;
        },
        replaceStyle: function(params, node)
        {
            params = this.inline.getStyleParams(params);

                    var self = this;
            return this.inline.eachInline(node, function(el)
            {
                var $el = $(el);
                        $el.removeAttr('style').css(params);

                        var style = $el.attr('style');
                        if (style) $el.attr('style', style.replace(/"/g, '\''));

                        $el = self.inline.addParentStyle($el);

                        return $el;
            });
        },
        toggleStyle: function(params, node)
        {
            params = this.inline.getStyleParams(params);

                    var self = this;
            return this.inline.eachInline(node, function(el)
            {
                var $el = $(el);

                        for (var key in params)
                        {
                            var newVal = params[key];
                            var oldVal = $el.css(key);

                            oldVal = (self.utils.isRgb(oldVal)) ? self.utils.rgb2hex(oldVal) : oldVal.replace(/"/g, '');
                            newVal = (self.utils.isRgb(newVal)) ? self.utils.rgb2hex(newVal) : newVal.replace(/"/g, '');

                            if (oldVal === newVal)
                            {
                                $el.css(key, '');
                            }
                            else
                            {
                                $el.css(key, newVal);
                            }
                        }

                        var style = $el.attr('style');
                        if (style) $el.attr('style', style.replace(/"/g, '\''));

                        if (!self.utils.removeEmptyAttr(el, 'style'))
                        {
                            $el = self.inline.addParentStyle($el);
                        }
                        else
                        {
                            $el.removeAttr('data-redactor-style-cache');
                        }


                        return $el;
            });
        },
        addStyle: function(params, node)
        {
                    params = this.inline.getStyleParams(params);

                    var self = this;
            return this.inline.eachInline(node, function(el)
            {
                        var $el = $(el);

                        $el.css(params);

                        var style = $el.attr('style');
                        if (style) $el.attr('style', style.replace(/"/g, '\''));

                        $el = self.inline.addParentStyle($el);

                        return $el;
            });
        },
        removeStyle: function(params, node)
        {
            params = this.inline.getStyleParams(params);

                    var self = this;
            return this.inline.eachInline(node, function(el)
            {
                var $el = $(el);

                        for (var key in params)
                        {
                            $el.css(key, '');
                        }

                        if (self.utils.removeEmptyAttr(el, 'style'))
                        {
                            $el.removeAttr('data-redactor-style-cache');
                        }
                        else
                        {
                            $el.attr('data-redactor-style-cache', $el.attr('style'));
                        }

                        return $el;
            });
        },
        removeAllStyle: function(node)
        {
            return this.inline.eachInline(node, function(el)
            {
                        return $(el).removeAttr('style').removeAttr('data-redactor-style-cache');
            });
        },
                removeStyleRule: function(name)
        {
          var parent = this.selection.parent();
          var nodes = this.selection.inlines();

                    this.buffer.set();

          if (parent && parent.tagName === 'SPAN')
          {
            this.inline.removeStyleRuleAttr($(parent), name);
          }

                    for (var i = 0; i < nodes.length; i++)
          {
              var el = nodes[i];
            var $el = $(el);
            if ($.inArray(el.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$el.hasClass('redactor-selection-marker'))
            {
                            this.inline.removeStyleRuleAttr($el, name);
            }
          }

        },
        removeStyleRuleAttr: function($el, name)
        {
                    $el.css(name, '');
          if (this.utils.removeEmptyAttr($el, 'style'))
          {
            $el.removeAttr('data-redactor-style-cache');
          }
          else
          {
              $el.attr('data-redactor-style-cache', $el.attr('style'));
          }
        },

        // Update
        update: function(tag, attr, value, type)
        {
                    tag = this.inline.arrangeTag(tag);

          var params = this.inline.getParams(attr, value, type);
          var nodes = this.selection.inlines();
          var result = [];

                    if (nodes)
                    {
                        for (var i = 0; i < nodes.length; i++)
                        {
                            var el = nodes[i];
                            if (tag === '*' || el.tagName.toLowerCase() === tag)
                            {
                    result.push(this.inline.setParams(el, params));
                }
                        }
                    }

                    return result;
        },

                // All
        removeFormat: function()
        {
            this.selection.save();

            var nodes = this.inline.getClearedNodes();
            for (var i = 0; i < nodes.length; i++)
            {
                if (nodes[i].nodeType === 1)
                {
                    $(nodes[i]).replaceWith(function()
                            {
                                return $(this).contents();
                            });
                }
            }

            this.selection.restore();
        }

      };
    },

    // =insert
    insert: function()
    {
      return {
        set: function(html)
        {
          this.placeholder.hide();

          this.code.set(html);
          this.focus.end();

          this.placeholder.enable();
        },
        html: function(html, data)
        {
          this.placeholder.hide();
          this.core.editor().focus();

          var block = this.selection.block();
          var inline = this.selection.inline();

          // clean
          if (typeof data === 'undefined')
          {
            data = this.clean.getCurrentType(html, true);
            html = this.clean.onPaste(html, data, true);
          }

          html = $.parseHTML(html);

                    // end node
                    var endNode = $(html).last();

          // delete selected content
          var sel = this.selection.get();
          var range = this.selection.range(sel);
          range.deleteContents();

          this.selection.update(sel, range);

          // insert list in list
          if (data.lists)
          {
            var $list = $(html);
            if ($list.length !== 0 && ($list[0].tagName === 'UL' || $list[0].tagName === 'OL'))
            {

              this.insert.appendLists(block, $list);
              return;
            }
          }

          if (data.blocks && block)
          {
            if (this.utils.isSelectAll())
            {
              this.core.editor().html(html);
              this.focus.end();
            }
            else
            {
              var breaked = this.utils.breakBlockTag();
              if (breaked === false)
              {
                this.insert.placeHtml(html);
              }
              else
              {
                  var $last = $(html).children().last();
                $last.append(this.marker.get());

                if (breaked.type === 'start')
                {
                  breaked.$block.before(html);
                }
                else
                {
                  breaked.$block.after(html);
                }

                this.selection.restore();
                this.core.editor().find('p').each(function()
                {
                  if ($.trim(this.innerHTML) === '')
                  {
                    $(this).remove();
                  }
                });
              }
            }
          }
          else
          {
            if (inline)
            {
              // remove same tag inside
              var $div = $("<div/>").html(html);
              $div.find(inline.tagName.toLowerCase()).each(function()
              {
                $(this).contents().unwrap();
              });

              html = $div.html();

            }

            if (this.utils.isSelectAll())
            {
              var $node = $(this.opts.emptyHtml);
              this.core.editor().html('').append($node);
              $node.html(html);
              this.caret.end($node);
            }
            else
            {
              this.insert.placeHtml(html);
            }
          }

          this.utils.disableSelectAll();

          if (data.pre) this.clean.cleanPre();

                    this.caret.end(endNode);
                    this.linkify.format();
        },
        text: function(text)
        {
          text = text.toString();
          text = $.trim(text);

          var tmp = document.createElement('div');
          tmp.innerHTML = text;
          text = tmp.textContent || tmp.innerText;

          if (typeof text === 'undefined')
          {
            return;
          }

          this.placeholder.hide();
          this.core.editor().focus();

          // blocks
          var blocks = this.selection.blocks();

          // nl to spaces
          text = text.replace(/\n/g, ' ');

          // select all
          if (this.utils.isSelectAll())
          {
            var $node = $(this.opts.emptyHtml);
            this.core.editor().html('').append($node);
            $node.html(text);
            this.caret.end($node);
          }
          else
          {
            // insert
            var sel = this.selection.get();
            var node = document.createTextNode(text);

            if (sel.getRangeAt && sel.rangeCount)
            {
              var range = sel.getRangeAt(0);
              range.deleteContents();
              range.insertNode(node);
              range.setStartAfter(node);
              range.collapse(true);

              this.selection.update(sel, range);
            }

            // wrap node if selected two or more block tags
            if (blocks.length > 1)
            {
              $(node).wrap('<p>');
              this.caret.after(node);
            }
          }

          this.utils.disableSelectAll();
          this.linkify.format();
          this.clean.normalizeCurrentHeading();

        },
        raw: function(html)
        {
          this.placeholder.hide();
          this.core.editor().focus();

          var sel = this.selection.get();

          var range = this.selection.range(sel);
          range.deleteContents();

                var el = document.createElement("div");
                el.innerHTML = html;

                var frag = document.createDocumentFragment(), node, lastNode;
                while ((node = el.firstChild))
                {
                    lastNode = frag.appendChild(node);
                }

                range.insertNode(frag);

          if (lastNode)
          {
            range = range.cloneRange();
            range.setStartAfter(lastNode);
            range.collapse(true);
            sel.removeAllRanges();
            sel.addRange(range);
          }
        },
        node: function(node, deleteContent)
        {
          this.placeholder.hide();

          if (typeof this.start !== 'undefined')
          {
            this.core.editor().focus();
          }

          node = node[0] || node;

          var block = this.selection.block();
          var gap = this.utils.isBlockTag(node.tagName);
          var result = true;

          if (this.utils.isSelectAll())
          {
            if (gap)
            {
              this.core.editor().html(node);
            }
            else
            {
              this.core.editor().html($('<p>').html(node));
            }

            this.code.sync();
          }
          else if (gap && block)
          {
            var breaked = this.utils.breakBlockTag();
            if (breaked === false)
            {
              this.insert.placeNode(node, deleteContent);
            }
            else
            {
              if (breaked.type === 'start')
              {
                breaked.$block.before(node);
              }
              else
              {
                breaked.$block.after(node);
              }

              this.core.editor().find('p:empty').remove();
            }
          }
          else
          {
            result = this.insert.placeNode(node, deleteContent);
          }

          this.utils.disableSelectAll();

          if (result)
          {
              this.caret.end(node);
          }

          return node;

        },
        appendLists: function(block, $list)
        {
          var $block = $(block);
          var last;
          var isEmpty = this.utils.isEmpty(block.innerHTML);

          if (isEmpty || this.utils.isEndOfElement(block))
          {
            last = $block;
            $list.find('li').each(function()
            {
              last.after(this);
              last = $(this);
            });

            if (isEmpty)
            {
              $block.remove();
            }
          }
          else if (this.utils.isStartOfElement(block))
          {
            $list.find('li').each(function()
            {
              $block.before(this);
              last = $(this);
            });
          }
          else
          {
                var endOfNode = this.selection.extractEndOfNode(block);

                $block.after($('<li>').append(endOfNode));
                $block.append($list);
            last = $list;
          }

          this.marker.remove();

          if (last)
          {
            this.caret.end(last);
          }

          this.linkify.format();
        },
        placeHtml: function(html)
        {
          var marker = document.createElement('span');
          marker.id = 'redactor-insert-marker';
          marker = this.insert.node(marker);

          $(marker).before(html);
          this.selection.restore();
          this.caret.after(marker);
          $(marker).remove();
        },
        placeNode: function(node, deleteContent)
        {
          var sel = this.selection.get();
          var range = this.selection.range(sel);
          if (range == null)
          {
              return false;
          }

          if (deleteContent !== false)
          {
            range.deleteContents();
          }

          range.insertNode(node);
          range.collapse(false);

          this.selection.update(sel, range);
        },
        nodeToPoint: function(e, node)
        {
          this.placeholder.hide();

          node = node[0] || node;

          if (this.utils.isEmpty())
          {
            node = (this.utils.isBlock(node)) ? node : $('<p />').append(node);

            this.core.editor().html(node);

            return node;
          }

          var range;
          var x = e.clientX, y = e.clientY;
          if (document.caretPositionFromPoint)
          {
              var pos = document.caretPositionFromPoint(x, y);
              var sel = document.getSelection();
              range = sel.getRangeAt(0);
              range.setStart(pos.offsetNode, pos.offset);
              range.collapse(true);
              range.insertNode(node);
          }
          else if (document.caretRangeFromPoint)
          {
              range = document.caretRangeFromPoint(x, y);
              range.insertNode(node);
          }
          else if (typeof document.body.createTextRange !== "undefined")
          {
                range = document.body.createTextRange();
                range.moveToPoint(x, y);
                var endRange = range.duplicate();
                endRange.moveToPoint(x, y);
                range.setEndPoint("EndToEnd", endRange);
                range.select();
          }

          return node;

        },

        // #backward
        nodeToCaretPositionFromPoint: function(e, node)
        {
          this.insert.nodeToPoint(e, node);
        },
        marker: function()
        {
          this.marker.insert();
        }
      };
    },

    // =keydown
    keydown: function()
    {
      return {
        init: function(e)
        {
          if (this.rtePaste)
          {
            return;
          }

          var key = e.which;
          var arrow = (key >= 37 && key <= 40);

          this.keydown.ctrl = e.ctrlKey || e.metaKey;
          this.keydown.parent = this.selection.parent();
          this.keydown.current = this.selection.current();
          this.keydown.block = this.selection.block();

              // detect tags
          this.keydown.pre = this.utils.isTag(this.keydown.current, 'pre');
          this.keydown.blockquote = this.utils.isTag(this.keydown.current, 'blockquote');
          this.keydown.figcaption = this.utils.isTag(this.keydown.current, 'figcaption');
          this.keydown.figure = this.utils.isTag(this.keydown.current, 'figure');

          // callback
          var keydownStop = this.core.callback('keydown', e);
          if (keydownStop === false)
          {
            e.preventDefault();
            return false;
          }

          // shortcuts setup
          this.shortcuts.init(e, key);

          // buffer
          this.keydown.checkEvents(arrow, key);
                    this.keydown.setupBuffer(e, key);

          if (this.utils.isSelectAll() && ( key === this.keyCode.ENTER || key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE))
          {
            e.preventDefault();

            this.code.set(this.opts.emptyHtml);
                        this.events.changeHandler();
            return;
          }

          this.keydown.addArrowsEvent(arrow);
          this.keydown.setupSelectAll(e, key);

          // turn off enter key
          if (!this.opts.enterKey && key === this.keyCode.ENTER)
          {
            e.preventDefault();

            // remove selected
            var sel = this.selection.get();
            var range = this.selection.range(sel);

            if (!range.collapsed)
            {
              range.deleteContents();
            }

            return;
          }

          // down
          if (this.opts.enterKey && key === this.keyCode.DOWN)
          {
            this.keydown.onArrowDown();
          }

          // up
          if (this.opts.enterKey && key === this.keyCode.UP)
          {
            this.keydown.onArrowUp();
          }


          // replace to p before / after the table or into body
          if ((this.opts.type === 'textarea' || this.opts.type === 'div') && this.keydown.current && this.keydown.current.nodeType === 3 && $(this.keydown.parent).hasClass('redactor-in'))
          {
            this.keydown.wrapToParagraph();
          }

          // on Shift+Space or Ctrl+Space
          if (!this.keyup.lastShiftKey && key === this.keyCode.SPACE && (e.ctrlKey || e.shiftKey))
          {
            e.preventDefault();

            return this.keydown.onShiftSpace();
          }

          // on Shift+Enter or Ctrl+Enter
          if (key === this.keyCode.ENTER && (e.ctrlKey || e.shiftKey))
          {
            e.preventDefault();

            return this.keydown.onShiftEnter(e);
          }

          // on enter
          if (key === this.keyCode.ENTER && !e.shiftKey && !e.ctrlKey && !e.metaKey)
          {
            return this.keydown.onEnter(e);
          }

          // tab or cmd + [
          if (key === this.keyCode.TAB || e.metaKey && key === 221 || e.metaKey && key === 219)
          {
            return this.keydown.onTab(e, key);
          }

                    // firefox bugfix
          if (this.detect.isFirefox() && key === this.keyCode.BACKSPACE && this.keydown.block && this.keydown.block.tagName === 'P' && this.utils.isStartOfElement(this.keydown.block))
          {
              var $prev = $(this.keydown.block).prev();
              if ($prev.length !== 0)
              {
                  e.preventDefault();

                  $prev.append(this.marker.get());
                  $prev.append($(this.keydown.block).html());
                  $(this.keydown.block).remove();

                            this.selection.restore();

                  return;
              }
          }

          // backspace & delete
          if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE)
          {
              if (this.observe.image && typeof this.observe.image !== 'undefined' && $('#redactor-image-box').length !== 0)
              {
                  e.preventDefault();

                  var $prev = this.observe.image.closest('figure, p').prev()
                  this.image.remove(false);
                            this.observe.image = false;

                  if ($prev && $prev.length !== 0)
                  {
                      this.caret.end($prev);
                  }
                  else
                  {
                      this.core.editor().focus();
                  }

                  return;
              }

            this.keydown.onBackspaceAndDeleteBefore();
          }

          if (key === this.keyCode.DELETE)
          {
            var $next = $(this.keydown.block).next();

            // delete figure
            if (this.utils.isEndOfElement(this.keydown.block) && $next.length !== 0 && $next[0].tagName === 'FIGURE')
            {
              $next.remove();
              return false;
            }

            // append list (safari bug)
            var tagLi = (this.keydown.block && this.keydown.block.tagName === 'LI') ? this.keydown.block : false;
            if (tagLi)
            {
              var $list = $(this.keydown.block).parents('ul, ol').last();
              var $nextList = $list.next();

              if (this.utils.isRedactorParent($list) && this.utils.isEndOfElement($list) && $nextList.length !== 0
                && ($nextList[0].tagName === 'UL' || $nextList[0].tagName === 'OL'))
              {
                e.preventDefault();

                $list.append($nextList.contents());
                $nextList.remove();

                return false;
              }
            }

            // append pre
            if (this.utils.isEndOfElement(this.keydown.block) && $next.length !== 0 && $next[0].tagName === 'PRE')
            {
              $(this.keydown.block).append($next.text());
              $next.remove();
              return false;
            }

          }

          // image delete
          if (key === this.keyCode.DELETE && $('#redactor-image-box').length !== 0)
          {
            this.image.remove();
          }

          // backspace
          if (key === this.keyCode.BACKSPACE)
          {
            if (this.detect.isFirefox())
            {
              this.line.removeOnBackspace(e);
            }

                        // combine list after and before if paragraph is empty
                        if (this.list.combineAfterAndBefore(this.keydown.block))
                        {
                            e.preventDefault();
                            return;
                        }

            // backspace as outdent
            var block = this.selection.block();
            if (block && block.tagName === 'LI' && this.utils.isCollapsed() && this.utils.isStartOfElement())
            {
              this.indent.decrease();
              e.preventDefault();
              return;
            }

            this.keydown.removeInvisibleSpace();
            this.keydown.removeEmptyListInTable(e);

          }

          if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE)
          {
            this.keydown.onBackspaceAndDeleteAfter(e);
          }

        },
        onShiftSpace: function()
        {
          this.buffer.set();
          this.insert.raw('&nbsp;');

          return false;
        },
        onShiftEnter: function(e)
        {
          this.buffer.set();

          return (this.keydown.pre) ? this.keydown.insertNewLine(e) : this.insert.raw('<br>');
        },
        onBackspaceAndDeleteBefore: function()
        {
          this.utils.saveScroll();
        },
        onBackspaceAndDeleteAfter: function(e)
        {
          // remove style tag
          setTimeout($.proxy(function()
          {
            this.code.syncFire = false;
            this.keydown.removeEmptyLists();

                        var filter = '';
                        if (this.opts.keepStyleAttr.length !== 0)
                        {
                            filter = ',' + this.opts.keepStyleAttr.join(',');
                        }

            var $styleTags = this.core.editor().find('*[style]');
            $styleTags.not('img, figure, iframe, #redactor-image-box, #redactor-image-editter, [data-redactor-style-cache], [data-redactor-span]' + filter).removeAttr('style');

            this.keydown.formatEmpty(e);
            this.code.syncFire = true;

          }, this), 1);
        },
        onEnter: function(e)
        {
          var stop = this.core.callback('enter', e);
          if (stop === false)
          {
            e.preventDefault();
            return false;
          }

          // blockquote exit
          if (this.keydown.blockquote && this.keydown.exitFromBlockquote(e) === true)
          {
            return false;
          }

          // pre
          if (this.keydown.pre)
          {
            return this.keydown.insertNewLine(e);
          }
          // blockquote & figcaption
          else if (this.keydown.blockquote || this.keydown.figcaption)
          {
            return this.keydown.insertBreakLine(e);
          }
          // figure
          else if (this.keydown.figure)
          {
            setTimeout($.proxy(function()
            {
              this.keydown.replaceToParagraph('FIGURE');

            }, this), 1);
          }
          // paragraphs
          else if (this.keydown.block)
          {
            setTimeout($.proxy(function()
            {
              this.keydown.replaceToParagraph('DIV');

            }, this), 1);

            // empty list exit
            if (this.keydown.block.tagName === 'LI')
            {
              var current = this.selection.current();
              var $parent = $(current).closest('li', this.$editor[0]);
              var $list = $parent.parents('ul,ol', this.$editor[0]).last();

              if ($parent.length !== 0 && this.utils.isEmpty($parent.html()) && $list.next().length === 0 && this.utils.isEmpty($list.find("li").last().html()))
              {
                $list.find("li").last().remove();

                var node = $(this.opts.emptyHtml);
                $list.after(node);
                this.caret.start(node);

                return false;
              }
            }

          }
          // outside
          else if (!this.keydown.block)
          {
            return this.keydown.insertParagraph(e);
          }

                    // firefox enter into inline element
          if (this.detect.isFirefox() && this.utils.isInline(this.keydown.parent))
          {
                        this.keydown.insertBreakLine(e);
                        return;
                    }


          // remove inline tags in new-empty paragraph
          setTimeout($.proxy(function()
          {
            var inline = this.selection.inline();
            if (inline && this.utils.isEmpty(inline.innerHTML))
            {
              var parent = this.selection.block();
              $(inline).remove();
              //this.caret.start(parent);

                            var range = document.createRange();
                            range.setStart(parent, 0);

                            var textNode = document.createTextNode('\u200B');

                            range.insertNode(textNode);
                            range.setStartAfter(textNode);
                            range.collapse(true);

                            var sel = window.getSelection();
                    sel.removeAllRanges();
                    sel.addRange(range);
            }


          }, this), 1);
        },
        checkEvents: function(arrow, key)
        {
          if (!arrow && (this.core.getEvent() === 'click' || this.core.getEvent() === 'arrow'))
          {
            this.core.addEvent(false);

            if (this.keydown.checkKeyEvents(key))
            {
              this.buffer.set();
            }
          }
        },
        checkKeyEvents: function(key)
        {
          var k = this.keyCode;
          var keys = [k.BACKSPACE, k.DELETE, k.ENTER, k.ESC, k.TAB, k.CTRL, k.META, k.ALT, k.SHIFT];

          return ($.inArray(key, keys) === -1) ? true : false;

        },
        addArrowsEvent: function(arrow)
        {
          if (!arrow)
          {
            return;
          }

          if ((this.core.getEvent() === 'click' || this.core.getEvent() === 'arrow'))
          {
            this.core.addEvent(false);
            return;
          }

            this.core.addEvent('arrow');
        },
        setupBuffer: function(e, key)
        {
          if (this.keydown.ctrl && key === 90 && !e.shiftKey && !e.altKey && this.sBuffer.length) // z key
          {
            e.preventDefault();
            this.buffer.undo();
            return;
          }
          // redo
          else if (this.keydown.ctrl && key === 90 && e.shiftKey && !e.altKey && this.sRebuffer.length !== 0)
          {
            e.preventDefault();
            this.buffer.redo();
            return;
          }
          else if (!this.keydown.ctrl)
          {
            if (key === this.keyCode.SPACE || key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE || (key === this.keyCode.ENTER && !e.ctrlKey && !e.shiftKey))
            {
              this.buffer.set();
            }
          }
        },
        exitFromBlockquote: function(e)
        {
          if (!this.utils.isEndOfElement(this.keydown.blockquote))
          {
            return;
          }

          var tmp = this.clean.removeSpacesHard($(this.keydown.blockquote).html());
          if (tmp.search(/(<br\s?\/?>){1}$/i) !== -1)
          {
            e.preventDefault();

            var $last = $(this.keydown.blockquote).children().last();

            $last.filter('br').remove();
            $(this.keydown.blockquote).children().last().filter('span').remove();

            var node = $(this.opts.emptyHtml);
            $(this.keydown.blockquote).after(node);
            this.caret.start(node);

            return true;

          }

          return;
        },
        onArrowDown: function()
        {
          var tags = [this.keydown.blockquote, this.keydown.pre, this.keydown.figcaption];

          for (var i = 0; i < tags.length; i++)
          {
            if (tags[i])
            {
              this.keydown.insertAfterLastElement(tags[i]);
              return false;
            }
          }
        },
        onArrowUp: function()
        {
          var tags = [this.keydown.blockquote, this.keydown.pre, this.keydown.figcaption];

          for (var i = 0; i < tags.length; i++)
          {
            if (tags[i])
            {
              this.keydown.insertBeforeFirstElement(tags[i]);
              return false;
            }
          }
        },
        insertAfterLastElement: function(element)
        {
          if (!this.utils.isEndOfElement(element))
          {
            return;
          }

          var last = this.core.editor().contents().last();
          var $next = (element.tagName === 'FIGCAPTION') ? $(this.keydown.block).parent().next() : $(this.keydown.block).next();

          if ($next.length !== 0)
          {
            return;
          }
          else if (last.length === 0 && last[0] !== element)
          {
            this.caret.start(last);
            return;
          }
          else
          {
            var node = $(this.opts.emptyHtml);

            if (element.tagName === 'FIGCAPTION')
            {
                $(element).parent().after(node);
            }
            else
            {
                $(element).after(node);
            }

            this.caret.start(node);
          }

        },
        insertBeforeFirstElement: function(element)
        {
          if (!this.utils.isStartOfElement())
          {
            return;
          }

          if (this.core.editor().contents().length > 1 && this.core.editor().contents().first()[0] !== element)
          {
            return;
          }

          var node = $(this.opts.emptyHtml);
          $(element).before(node);
          this.caret.start(node);

        },
        onTab: function(e, key)
        {
          if (!this.opts.tabKey)
          {
            return true;
          }

          var isList = (this.keydown.block && this.keydown.block.tagName === 'LI')
          if (this.utils.isEmpty(this.code.get()) || (!isList && !this.keydown.pre && this.opts.tabAsSpaces === false))
          {
            return true;
          }

          e.preventDefault();
          this.buffer.set();

                    var isListStart = (isList && this.utils.isStartOfElement(this.keydown.block));
          var node;

          if (this.keydown.pre && !e.shiftKey)
          {
            node = (this.opts.preSpaces) ? document.createTextNode(Array(this.opts.preSpaces + 1).join('\u00a0')) : document.createTextNode('\t');
            this.insert.node(node);
          }
          else if (this.opts.tabAsSpaces !== false && !isListStart)
          {
            node = document.createTextNode(Array(this.opts.tabAsSpaces + 1).join('\u00a0'));
            this.insert.node(node);
          }
          else
          {
            if (e.metaKey && key === 219)
            {
              this.indent.decrease();
            }
            else if (e.metaKey && key === 221)
            {
              this.indent.increase();
            }
            else if (!e.shiftKey)
            {
              this.indent.increase();
            }
            else
            {
              this.indent.decrease();
            }
          }

          return false;
        },
        setupSelectAll: function(e, key)
        {
          if (this.keydown.ctrl && key === 65)
          {
            this.utils.enableSelectAll();
          }
          else if (key !== this.keyCode.LEFT_WIN && !this.keydown.ctrl)
          {
            this.utils.disableSelectAll();
          }
        },
        insertNewLine: function(e)
        {
          e.preventDefault();

          var node = document.createTextNode('\n');

          var sel = this.selection.get();
          var range = this.selection.range(sel);

          range.deleteContents();
          range.insertNode(node);

          this.caret.after(node);

          return false;
        },
        insertParagraph: function(e)
        {
          e.preventDefault();

          var p = document.createElement('p');
          //p.innerHTML = this.opts.invisibleSpace;
          p.innerHTML = '<br>';

          var sel = this.selection.get();
          var range = this.selection.range(sel);

          range.deleteContents();
          range.insertNode(p);

          this.caret.start(p);

          return false;
        },
        insertBreakLine: function(e)
        {
          return this.keydown.insertBreakLineProcessing(e);
        },
        insertDblBreakLine: function(e)
        {
          return this.keydown.insertBreakLineProcessing(e, true);
        },
        insertBreakLineProcessing: function(e, dbl)
        {
          e.stopPropagation();

          var br1 = document.createElement('br');
          this.insert.node(br1);

          if (dbl === true)
          {
            var br2 = document.createElement('br');
            this.insert.node(br2);
            this.caret.after(br2);
          }
          else
          {
              this.caret.after(br1);
          }

          return false;

        },
        wrapToParagraph: function()
        {
          var $current = $(this.keydown.current);
          var node = $('<p>').append($current.clone());
          $current.replaceWith(node);

          var next = $(node).next();
          if (typeof(next[0]) !== 'undefined' && next[0].tagName === 'BR')
          {
            next.remove();
          }

          this.caret.end(node);

        },
        replaceToParagraph: function(tag)
        {
          var blockElem = this.selection.block();
          var $prev = $(blockElem).prev();

          var blockHtml = blockElem.innerHTML.replace(/<br\s?\/?>/gi, '');
          if (blockElem.tagName === tag && this.utils.isEmpty(blockHtml) && !$(blockElem).hasClass('redactor-in'))
          {
            var p = document.createElement('p');
            $(blockElem).replaceWith(p);

                        this.keydown.setCaretToParagraph(p);

            return false;
          }
          else if (blockElem.tagName === 'P')
          {
            $(blockElem).removeAttr('class').removeAttr('style');

            // fix #227
            if (this.detect.isIe() && this.utils.isEmpty(blockHtml) && this.utils.isInline(this.keydown.parent))
                        {
                            $(blockElem).on('input', $.proxy(function()
                            {
                                var parent = this.selection.parent();
                                if (this.utils.isInline(parent))
                                {
                                    var html = $(parent).html();
                                    $(blockElem).html(html);
                                    this.caret.end(blockElem);
                                }

                                $(blockElem).off('keyup');

                            }, this));
            }

            return false;
          }
          else if ($prev.hasClass(this.opts.videoContainerClass))
          {
              $prev.removeAttr('class');

              var p = document.createElement('p');
            $prev.replaceWith(p);

            this.keydown.setCaretToParagraph(p);

            return false;
          }
        },
        setCaretToParagraph: function(p)
        {
                    var range = document.createRange();
                    range.setStart(p, 0);

                    var textNode = document.createTextNode('\u200B');

                    range.insertNode(textNode);
                    range.setStartAfter(textNode);
                    range.collapse(true);

                    var sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
        },
        removeInvisibleSpace: function()
        {
          var $current = $(this.keydown.current);
          if ($current.text().search(/^\u200B$/g) === 0)
          {
            $current.remove();
          }
        },
        removeEmptyListInTable: function(e)
        {
          var $current = $(this.keydown.current);
          var $parent = $(this.keydown.parent);
          var td = $current.closest('td', this.$editor[0]);

          if (td.length !== 0 && $current.closest('li', this.$editor[0]) && $parent.children('li').length === 1)
          {
            if (!this.utils.isEmpty($current.text()))
            {
              return;
            }

            e.preventDefault();

            $current.remove();
            $parent.remove();

            this.caret.start(td);
          }
        },
        removeEmptyLists: function()
        {
          var removeIt = function()
          {
            var html = $.trim(this.innerHTML).replace(/\/t\/n/g, '');
            if (html === '')
            {
              $(this).remove();
            }
          };

          this.core.editor().find('li').each(removeIt);
          this.core.editor().find('ul, ol').each(removeIt);
        },
        formatEmpty: function(e)
        {
          var html = $.trim(this.core.editor().html());

          if (!this.utils.isEmpty(html))
          {
            return;
          }

          e.preventDefault();

          if (this.opts.type === 'inline' || this.opts.type === 'pre')
          {
            this.core.editor().html(this.marker.html());
            this.selection.restore();
          }
          else
          {
            this.core.editor().html(this.opts.emptyHtml);
            this.focus.start();
          }

          return false;

        }
      };
    },

    // =keyup
    keyup: function()
    {
      return {
        init: function(e)
        {
          if (this.rtePaste)
          {
            return;
          }

          var key = e.which;
          this.keyup.block = this.selection.block();
          this.keyup.current = this.selection.current();
          this.keyup.parent = this.selection.parent();
          this.keyup.lastShiftKey = e.shiftKey;


          // callback
          var stop = this.core.callback('keyup', e);
          if (stop === false)
          {
            e.preventDefault();
            return false;
          }

                    // replace a prev figure to paragraph if caret is before image
                    if (key === this.keyCode.ENTER)
                    {
                        if (this.keyup.block && this.keyup.block.tagName === 'FIGURE')
                        {
                            var $prev = $(this.keyup.block).prev();
                            if ($prev.length !== 0 && $prev[0].tagName === 'FIGURE')
                            {
                                var $newTag = this.utils.replaceToTag($prev, 'p');
                                this.caret.start($newTag);
                                return;
                            }
                        }
                    }

          // replace figure to paragraph
          if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE)
          {
            if (this.utils.isSelectAll())
            {
              this.focus.start();
                            this.toolbar.setUnfixed();

              return;
            }

            // if caret before figure - delete image
            if (this.keyup.block && this.keydown.block && this.keyup.block.tagName === 'FIGURE' && this.utils.isStartOfElement(this.keydown.block))
            {
                e.preventDefault();

                            this.selection.save();
                            $(this.keyup.block).find('figcaption').remove();
                $(this.keyup.block).find('img').first().remove();
                this.utils.replaceToTag(this.keyup.block, 'p');

                var $marker = this.marker.find();
                $('html, body').animate({ scrollTop: $marker.position().top + 20 }, 500);

                this.selection.restore();
                return;
            }


            // if paragraph does contain only image replace to figure
            if (this.keyup.block && this.keyup.block.tagName === 'P')
            {
              var isContainImage = $(this.keyup.block).find('img').length;
              var text = $(this.keyup.block).text().replace(/\u200B/g, '');
              if (text === '' && isContainImage !== 0)
              {
                this.utils.replaceToTag(this.keyup.block, 'figure');
              }
            }

            // if figure does not contain image - replace to paragraph
            if (this.keyup.block && this.keyup.block.tagName === 'FIGURE' && $(this.keyup.block).find('img').length === 0)
            {
              this.selection.save();
              this.utils.replaceToTag(this.keyup.block, 'p');
              this.selection.restore();
            }
          }

          // linkify
          if (this.linkify.isKey(key))
          {
            this.linkify.format();
          }

        }

      };
    },

    // =lang
    lang: function()
    {
      return {
        load: function()
        {
          this.opts.curLang = this.opts.langs[this.opts.lang];
        },
        get: function(name)
        {
          return (typeof this.opts.curLang[name] !== 'undefined') ? this.opts.curLang[name] : '';
        }
      };
    },

    // =line
    line: function()
    {
      return {
        insert: function()
        {
          this.buffer.set();

          // insert
          this.insert.html(this.line.getLineHtml());

          // find
          var $hr = this.core.editor().find('#redactor-hr-tmp-id');
          $hr.removeAttr('id');

          this.core.callback('insertedLine', $hr);

          return $hr;
        },
        getLineHtml: function()
        {
          var html = '<hr id="redactor-hr-tmp-id" />';
          if (!this.detect.isFirefox() && this.utils.isEmpty())
          {
            html += '<p>' + this.opts.emptyHtml + '</p>';
          }

          return html;
        },
        // ff only
        removeOnBackspace: function(e)
        {
          if (!this.utils.isCollapsed())
          {
            return;
          }

          var $block = $(this.selection.block());
          if ($block.length === 0 || !this.utils.isStartOfElement($block))
          {
            return;
          }

          // if hr is previous element
          var $prev = $block.prev();
          if ($prev && $prev.length !== 0 && $prev[0].tagName === 'HR')
          {
            e.preventDefault();
            $prev.remove();
          }
        }
      };
    },

    // =link
    link: function()
    {
      return {

        // public
        get: function()
        {
          return $(this.selection.inlines('a'));
        },
        is: function()
        {
          var nodes = this.selection.nodes() ;
          var $link = $(this.selection.current()).closest('a', this.core.editor()[0]);

          return ($link.length === 0 || nodes.length > 1) ? false : $link;
        },
        unlink: function(e)
        {
          // if call from clickable element
          if (typeof e !== 'undefined' && e.preventDefault)
          {
            e.preventDefault();
          }

          // buffer
          this.buffer.set();

          var links = this.selection.inlines('a');
          if (links.length === 0)
          {
            return;
          }

          var $links = this.link.replaceLinksToText(links);

          this.observe.closeAllTooltip();
          this.core.callback('deletedLink', $links);

        },
        insert: function(link, cleaned)
        {
          var $el = this.link.is();

          if (cleaned !== true)
          {
            link = this.link.buildLinkFromObject($el, link);
            if (link === false)
            {
              return false;
            }
          }

          // buffer
          this.buffer.set();

          // callback
          link = this.core.callback('beforeInsertingLink', link);

          if ($el === false)
          {
            // insert
            $el = $('<a />');
            $el = this.link.update($el, link);
            $el = $(this.insert.node($el));

            var $parent = $el.parent();
            if (this.utils.isRedactorParent($parent) === false)
            {
              $el.wrap('<p>');
            }

            // remove unlink wrapper
            if ($parent.hasClass('redactor-unlink'))
            {
              $parent.replaceWith(function(){
                return $(this).contents();
              });
            }

            this.caret.after($el);
            this.core.callback('insertedLink', $el);
          }
          else
          {
            // update
            $el = this.link.update($el, link);
            this.caret.after($el);
          }

          return $el;

        },
        update: function($el, link)
        {
          $el.text(link.text);
          $el.attr('href', link.url);

          this.link.target($el, link.target);

          return $el;

        },
        target: function($el, target)
        {
          return (target) ? $el.attr('target', '_blank') : $el.removeAttr('target');
        },
        show: function(e)
        {
          // if call from clickable element
          if (typeof e !== 'undefined' && e.preventDefault)
          {
            e.preventDefault();
          }

          // close tooltip
          this.observe.closeAllTooltip();

          // is link
          var $el = this.link.is();

          // build modal
          this.link.buildModal($el);

          // build link
          var link = this.link.buildLinkFromElement($el);

          // if link cut & paste inside editor browser added self host to a link
          link.url = this.link.removeSelfHostFromUrl(link.url);

                    // new tab target
          if (this.opts.linkNewTab && !$el)
          {
              link.target = true;
          }

          // set modal values
          this.link.setModalValues(link);

          // show modal
          this.modal.show();

          // focus
          if (this.detect.isDesktop())
          {
            $('#redactor-link-url').focus();
          }
        },

        // private
        setModalValues: function(link)
        {
          $('#redactor-link-blank').prop('checked', link.target);
          $('#redactor-link-url').val(link.url);
          $('#redactor-link-url-text').val(link.text);
        },
        buildModal: function($el)
        {
          this.modal.load('link', this.lang.get(($el === false) ? 'link-insert' : 'link-edit'), 600);

          // button insert
          var $btn = this.modal.getActionButton();
          $btn.text(this.lang.get(($el === false) ? 'insert' : 'save')).on('click', $.proxy(this.link.callback, this));

        },
        callback: function()
        {
          // build link
          var link = this.link.buildLinkFromModal();
          if (link === false)
          {
            return false;
          }

          // close
          this.modal.close();

          // insert or update
          this.link.insert(link, true);
        },
        cleanUrl: function(url)
        {
          return (typeof url === 'undefined') ? '' : $.trim(url.replace(/[^\W\w\D\d+&\'@#/%?=~_|!:,.;\(\)]/gi, ''));
        },
        cleanText: function(text)
        {
          return (typeof text === 'undefined') ? '' :$.trim(text.replace(/(<([^>]+)>)/gi, ''));
        },
        getText: function(link)
        {
          return (link.text === '' && link.url !== '') ? this.link.truncateUrl(link.url.replace(/<|>/g, '')) : link.text;
        },
        isUrl: function(url)
        {
          var pattern = '((xn--)?[\\W\\w\\D\\d]+(-[\\W\\w\\D\\d]+)*\\.)+[\\W\\w]{2,}';

          var re1 = new RegExp('^(http|ftp|https)://' + pattern, 'i');
          var re2 = new RegExp('^' + pattern, 'i');
          var re3 = new RegExp('\.(html|php)$', 'i');
          var re4 = new RegExp('^/', 'i');
          var re5 = new RegExp('^tel:(.*?)', 'i');

          // add protocol
          if (url.search(re1) === -1 && url.search(re2) !== -1 && url.search(re3) === -1 && url.substring(0, 1) !== '/')
          {
            url = 'http://' + url;
          }

          if (url.search(re1) !== -1 || url.search(re3) !== -1 || url.search(re4) !== -1 || url.search(re5) !== -1)
          {
            return url;
          }

          return false;
        },
        isMailto: function(url)
        {
          return (url.search('@') !== -1 && /(http|ftp|https):\/\//i.test(url) === false);
        },
        isEmpty: function(link)
        {
          return (link.url === '' || (link.text === '' && link.url === ''));
        },
        truncateUrl: function(url)
        {
          return (url.length > this.opts.linkSize) ? url.substring(0, this.opts.linkSize) + '...' : url;
        },
        parse: function(link)
        {
          // mailto
          if (this.link.isMailto(link.url))
          {
            link.url = 'mailto:' + link.url.replace('mailto:', '');
          }
          // url
          else if (link.url.search('#') !== 0)
          {
            link.url = this.link.isUrl(link.url);
          }

          // empty url or text or isn't url
          return (this.link.isEmpty(link) || link.url === false) ? false : link;

        },
        buildLinkFromModal: function()
        {
          var link = {};

          // url
          link.url = this.link.cleanUrl($('#redactor-link-url').val());

          // text
          link.text = this.link.cleanText($('#redactor-link-url-text').val());
          link.text = this.link.getText(link);

          // target
          link.target = ($('#redactor-link-blank').prop('checked')) ? true : false;

          // parse
          return this.link.parse(link);

        },
        buildLinkFromObject: function($el, link)
        {
          // url
          link.url = this.link.cleanUrl(link.url);

          // text
          link.text = (typeof link.text === 'undefined' && this.selection.is()) ? this.selection.text() : this.link.cleanText(link.text);
          link.text = this.link.getText(link);

          // target
          link.target = ($el === false) ? link.target : this.link.buildTarget($el);

          // parse
          return this.link.parse(link);

        },
        buildLinkFromElement: function($el)
        {
          var link = {
            url: '',
            text: (this.selection.is()) ? this.selection.text() : '',
            target: false
          };

          if ($el !== false)
          {
            link.url = $el.attr('href');
            link.text = $el.text();
            link.target = this.link.buildTarget($el);
          }

          return link;
        },
        buildTarget: function($el)
        {
          return (typeof $el.attr('target') !== 'undefined' && $el.attr('target') === '_blank') ? true : false;
        },
        removeSelfHostFromUrl: function(url)
        {
          var href = self.location.href.replace('#', '').replace(/\/$/i, '');
          return url.replace(/^\/\#/, '#').replace(href, '').replace('mailto:', '');
        },
        replaceLinksToText: function(links)
        {
          var $first;
          var $links = $.each(links, function(i,s)
          {
            var $el = $(s);
            var $unlinked = $('<span class="redactor-unlink" />').append($el.contents());
            $el.replaceWith($unlinked);

            if (i === 0)
            {
              $first = $unlinked;
            }

            return $el;
          });

          // set caret after unlinked node
          if (links.length === 1 && this.selection.isCollapsed())
          {
            this.caret.after($first);
          }

          return $links;
        }
      };
    },

    // =linkify
    linkify: function()
    {
      return {
        isKey: function(key)
        {
          return key === this.keyCode.ENTER || key === this.keyCode.SPACE;
        },
        isLink: function(node)
        {
          return (node.nodeValue.match(this.opts.regexps.linkyoutube) || node.nodeValue.match(this.opts.regexps.linkvimeo) || node.nodeValue.match(this.opts.regexps.linkimage) || node.nodeValue.match(this.opts.regexps.url));
        },
        isFiltered: function(i, node)
        {
          return node.nodeType === 3 && $.trim(node.nodeValue) !== "" && !$(node).parent().is("pre") && (this.linkify.isLink(node));
        },
        handler: function(i, node)
        {
          var $el = $(node);
          var text = $el.text();
          var html = text;

          if (html.match(this.opts.regexps.linkyoutube) || html.match(this.opts.regexps.linkvimeo))
          {
            html = this.linkify.convertVideoLinks(html);
          }
          else if (html.match(this.opts.regexps.linkimage))
          {
            html = this.linkify.convertImages(html);
          }
          else
          {
            html = this.linkify.convertLinks(html);
          }

          $el.before(text.replace(text, html)).remove();
        },
        format: function()
        {
          if (!this.opts.linkify || this.utils.isCurrentOrParent('pre'))
          {
            return;
          }

          this.core.editor().find(":not(iframe,img,a,pre,code,.redactor-unlink)").addBack().contents().filter($.proxy(this.linkify.isFiltered, this)).each($.proxy(this.linkify.handler, this));

          // collect
          var $objects = this.core.editor().find('.redactor-linkify-object').each($.proxy(function(i,s)
          {
            var $el = $(s);
            $el.removeClass('redactor-linkify-object');
            if ($el.attr('class') === '')
            {
              $el.removeAttr('class');
            }

            if (s.tagName === 'DIV') // video container
            {
              this.linkify.breakBlockTag($el, 'video');
            }
            else if (s.tagName === 'IMG') // image
            {
              this.linkify.breakBlockTag($el, 'image');
            }
            else if (s.tagName === 'A')
            {
              this.core.callback('insertedLink', $el);
            }

            return $el;

          }, this));

          // callback
          setTimeout($.proxy(function()
          {
            this.code.sync();
            this.core.callback('linkify', $objects);

          }, this), 100);

        },
        breakBlockTag: function($el, type)
        {
          var breaked = this.utils.breakBlockTag();
          if (breaked === false)
          {
            return;
          }

          var $newBlock = $el;
          if (type === 'image')
          {
            $newBlock = $('<figure />').append($el);
          }

          if (breaked.type === 'start')
          {
            breaked.$block.before($newBlock);
          }
          else
          {
            breaked.$block.after($newBlock);
          }


          if (type === 'image')
          {
            this.caret.after($newBlock);
          }

        },
        convertVideoLinks: function(html)
        {
          var iframeStart = '<div class="' + this.opts.videoContainerClass + ' redactor-linkify-object"><iframe class="redactor-linkify-object" width="500" height="281" src="';
          var iframeEnd = '" frameborder="0" allowfullscreen></iframe></div>';

          if (html.match(this.opts.regexps.linkyoutube))
          {
            html = html.replace(this.opts.regexps.linkyoutube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd);
          }

          if (html.match(this.opts.regexps.linkvimeo))
          {
            html = html.replace(this.opts.regexps.linkvimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd);
          }

          return html;
        },
        convertImages: function(html)
        {
          var matches = html.match(this.opts.regexps.linkimage);
          if (!matches)
          {
            return html;
          }

          return html.replace(html, '<img src="' + matches + '" class="redactor-linkify-object" />');
        },
        convertLinks: function(html)
        {
          var matches = html.match(this.opts.regexps.url);
          if (!matches)
          {
            return html;
          }

          matches = $.grep(matches, function(v, k) { return $.inArray(v, matches) === k; });

          var length = matches.length;

          for (var i = 0; i < length; i++)
          {
            var href = matches[i], text = href;
            var linkProtocol = (href.match(/(https?|ftp):\/\//i) !== null) ? '' : 'http://';

            if (text.length > this.opts.linkSize)
            {
              text = text.substring(0, this.opts.linkSize) + '...';
            }

            if (text.search('%') === -1)
            {
              text = decodeURIComponent(text);
            }

            var regexB = "\\b";

            if ($.inArray(href.slice(-1), ["/", "&", "="]) !== -1)
            {
              regexB = "";
            }

            // escaping url
            var regexp = new RegExp('(' + href.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") + regexB + ')', 'g');

            // target
            var target = '';
            if (this.opts.pasteLinkTarget !== false)
                        {
                  target = ' target="' + this.opts.pasteLinkTarget + '"';
                        }

            html = html.replace(regexp, '<a href="' + linkProtocol + $.trim(href) + '"' + target + ' class="redactor-linkify-object">' + $.trim(text) + '</a>');
          }

          return html;
        }
      };
    },

    // =list
    list: function()
    {
      return {
        toggle: function(cmd)
        {
          if (this.utils.inBlocks(['table', 'td', 'th', 'tr']))
          {
            return;
          }

          var tag = (cmd === 'orderedlist' || cmd === 'ol') ? 'OL' : 'UL';
          cmd = (tag === 'OL') ? 'orderedlist' : 'unorderedlist'

          var $list = $(this.selection.current()).parentsUntil('.redactor-in', 'ul, ol').first();

          this.placeholder.hide();
          this.buffer.set();


          if ($list.length !== 0 && $list[0].tagName === tag && this.utils.isRedactorParent($list))
          {
            this.selection.save();

            // remove list
            $list.find('ul, ol').each(function()
            {
              var parent = $(this).closest('li');
              $(this).find('li').each(function()
              {
                $(parent).after(this);
              });
            });

            $list.find('ul, ol').remove();
            $list.find('li').each(function()
            {
              return $(this).replaceWith(function()
              {
                return $('<p />').append($(this).contents());
              });
            });


            $list.replaceWith(function()
            {
              return $(this).contents();
            });

            this.selection.restore();
            return;
          }


          this.selection.save();

          if ($list.length !== 0 && $list[0].tagName !== tag)
          {
                        $list.each($.proxy(function(i,s)
                        {
                            this.utils.replaceToTag(s, tag);

                        }, this));
          }
          else
          {
              document.execCommand('insert' + cmd);
          }

          this.selection.restore();

          var $insertedList = this.list.get();
          if (!$insertedList)
          {
            if (!this.selection.block())
            {
              document.execCommand('formatblock', false, 'p');
            }

            return;
          }

          // clear span
          $insertedList.find('span').replaceWith(function()
          {
            return $(this).contents();
          });

          // remove style
          $insertedList.find(this.opts.inlineTags.join(',')).each(function()
          {
            $(this).removeAttr('style');
          });

          // remove block-element list wrapper
          var $listParent = $insertedList.parent();
          if (this.utils.isRedactorParent($listParent) && $listParent[0].tagName !== 'LI' && this.utils.isBlock($listParent))
          {
            this.selection.save();

            $listParent.replaceWith($listParent.contents());

            this.selection.restore();
          }

        },
        get: function()
        {
          var current = this.selection.current();
          var $list = $(current).closest('ul, ol', this.core.editor()[0]);

          return ($list.length === 0) ? false : $list;
        },
        combineAfterAndBefore: function(block)
        {
            var $prev = $(block).prev();
            var $next = $(block).next();
                    var isEmptyBlock = (block && block.tagName === 'P' && (block.innerHTML === '<br>' || block.innerHTML === ''));
                    var isBlockWrapped = ($prev.closest('ol, ul', this.core.editor()[0]).length === 1 && $next.closest('ol, ul', this.core.editor()[0]).length === 1);

                    if (isEmptyBlock && isBlockWrapped)
                    {
                        $prev.children('li').last().append(this.marker.get());
                        $prev.append($next.contents());
                        this.selection.restore();

                        return true;
                    }

                    return false;

        }
      };
    },

    // =marker
    marker: function()
    {
      return {

        // public
        get: function(num)
        {
          num = (typeof num === 'undefined') ? 1 : num;

          var marker = document.createElement('span');

          marker.id = 'selection-marker-' + num;
          marker.className = 'redactor-selection-marker';
          marker.innerHTML = this.opts.invisibleSpace;

          return marker;
        },
        html: function(num)
        {
          return this.utils.getOuterHtml(this.marker.get(num));
        },
        find: function(num)
        {
          num = (typeof num === 'undefined') ? 1 : num;

          return this.core.editor().find('span#selection-marker-' + num);
        },
        insert: function()
        {
          var sel = this.selection.get();
          var range = this.selection.range(sel);

          this.marker.insertNode(range, this.marker.get(1), true);
          if (range && range.collapsed === false)
          {
            this.marker.insertNode(range, this.marker.get(2), false);
          }

        },
        remove: function()
        {
          this.core.editor().find('.redactor-selection-marker').each(this.marker.iterateRemove);
        },

        // private
        insertNode: function(range, node, collapse)
        {
          var parent = this.selection.parent();
          if (range === null || $(parent).closest('.redactor-in').length === 0)
          {
            return;
          }

          range = range.cloneRange();

          try {
            range.collapse(collapse);
            range.insertNode(node);
          }
          catch (e)
          {
            this.focus.start();
          }
        },
        iterateRemove: function(i, el)
        {
          var $el = $(el);
          var text = $el.text().replace(/\u200B/g, '');
          var parent = $el.parent()[0];

                    if (text === '') $el.remove();
                    else $el.replaceWith(function() { return $(this).contents(); });

                    // if (parent && parent.normalize) parent.normalize();
        }
      };
    },

    // =modal
    modal: function()
    {
      return {
        callbacks: {},
        templates: function()
        {
          this.opts.modal = {
            'image-edit': String()
            + '<div class="redactor-modal-tab redactor-group" data-title="General">'
              + '<div id="redactor-image-preview" class="redactor-modal-tab-side">'
              + '</div>'
              + '<div class="redactor-modal-tab-area">'
                + '<section>'
                  + '<label>' + this.lang.get('title') + '</label>'
                  + '<input type="text" id="redactor-image-title" />'
                + '</section>'
                + '<section>'
                  + '<label>' + this.lang.get('caption') + '</label>'
                  + '<input type="text" id="redactor-image-caption" aria-label="' + this.lang.get('caption') + '" />'
                + '</section>'
                + '<section>'
                  + '<label>' + this.lang.get('link') + '</label>'
                  + '<input type="text" id="redactor-image-link" aria-label="' + this.lang.get('link') + '" />'
                + '</section>'
                + '<section>'
                      + '<label class="redactor-image-position-option">' + this.lang.get('image-position') + '</label>'
                      + '<select class="redactor-image-position-option" id="redactor-image-align" aria-label="' + this.lang.get('image-position') + '">'
                        + '<option value="none">' + this.lang.get('none') + '</option>'
                        + '<option value="left">' + this.lang.get('left') + '</option>'
                        + '<option value="center">' + this.lang.get('center') + '</option>'
                        + '<option value="right">' + this.lang.get('right') + '</option>'
                      + '</select>'
                + '</section>'
                + '<section>'
                  + '<label class="checkbox"><input type="checkbox" id="redactor-image-link-blank" aria-label="' + this.lang.get('link-in-new-tab') + '"> ' + this.lang.get('link-in-new-tab') + '</label>'
                + '</section>'
                + '<section>'
                  + '<button id="redactor-modal-button-action">' + this.lang.get('insert') + '</button>'
                  + '<button id="redactor-modal-button-cancel">' + this.lang.get('cancel') + '</button>'
                  + '<button id="redactor-modal-button-delete" class="redactor-modal-button-offset">' + this.lang.get('delete') + '</button>'
                + '</section>'
              + '</div>'
            + '</div>',

            'image': String()
            + '<div class="redactor-modal-tab" data-title="Upload">'
              + '<section>'
                + '<div id="redactor-modal-image-droparea"></div>'
              + '</section>'
            + '</div>',

            'file': String()
            + '<div class="redactor-modal-tab" data-title="Upload">'
              + '<section>'
                + '<label>' + this.lang.get('filename') + ' <span class="desc">(' + this.lang.get('optional') + ')</span></label>'
                + '<input type="text" id="redactor-filename" aria-label="' + this.lang.get('filename') + '" /><br><br>'
              + '</section>'
              + '<section>'
                + '<div id="redactor-modal-file-upload"></div>'
              + '</section>'
            + '</div>',

            'link': String()
            + '<div class="redactor-modal-tab" data-title="General">'
              + '<section>'
                + '<label>URL</label>'
                + '<input type="url" id="redactor-link-url" aria-label="URL" />'
              + '</section>'
              + '<section>'
                + '<label>' + this.lang.get('text') + '</label>'
                + '<input type="text" id="redactor-link-url-text" aria-label="' + this.lang.get('text') + '" />'
              + '</section>'
              + '<section>'
                + '<label class="checkbox"><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get('link-in-new-tab') + '</label>'
              + '</section>'
              + '<section>'
                + '<button id="redactor-modal-button-action">' + this.lang.get('insert') + '</button>'
                + '<button id="redactor-modal-button-cancel">' + this.lang.get('cancel') + '</button>'
              + '</section>'
            + '</div>'
          };

          $.extend(this.opts, this.opts.modal);

        },
        addCallback: function(name, callback)
        {
          this.modal.callbacks[name] = callback;
        },
        addTemplate: function(name, template)
        {
          this.opts.modal[name] = template;
        },
        getTemplate: function(name)
        {
          return this.opts.modal[name];
        },
        getModal: function()
        {
          return this.$modalBody;
        },
        getActionButton: function()
        {
          return this.$modalBody.find('#redactor-modal-button-action');
        },
        getCancelButton: function()
        {
          return this.$modalBody.find('#redactor-modal-button-cancel');
        },
        getDeleteButton: function()
        {
          return this.$modalBody.find('#redactor-modal-button-delete');
        },
        load: function(templateName, title, width)
        {
          if (typeof this.$modalBox !== 'undefined' && this.$modalBox.hasClass('open'))
          {
            return;
          }

          this.modal.templateName = templateName;
          this.modal.width = width;

          this.modal.build();
          this.modal.enableEvents();
          this.modal.setTitle(title);
          this.modal.setDraggable();
          this.modal.setContent();

          // callbacks
          if (typeof this.modal.callbacks[templateName] !== 'undefined')
          {
            this.modal.callbacks[templateName].call(this);
          }

        },
        show: function()
        {

          if (!this.detect.isDesktop())
          {
            document.activeElement.blur();
          }

          this.selection.save();
          this.modal.buildTabber();

          if (this.detect.isMobile())
          {
            this.modal.width = '96%';
          }

          // resize
          setTimeout($.proxy(this.modal.buildWidth, this), 0);
          $(window).on('resize.redactor-modal', $.proxy(this.modal.buildWidth, this));


          this.$modalOverlay.redactorAnimation('fadeIn', {
            duration: 0.25
          });

          this.$modalBox.addClass('open').show();
          this.$modal.redactorAnimation('fadeIn', {
              timing: 'cubic-bezier(0.175, 0.885, 0.320, 1.105)'
            },
            $.proxy(function()
            {

              this.utils.saveScroll();
              this.utils.disableBodyScroll();

              // modal shown callback
              this.core.callback('modalOpened', this.modal.templateName, this.$modal);

              // fix bootstrap modal focus
              $(document).off('focusin.modal');

              // enter
              var $elements = this.$modal.find('input[type=text],input[type=url],input[type=email]');
              $elements.on('keydown.redactor-modal', $.proxy(this.modal.setEnter, this));

            }, this)
          );

        },
        buildWidth: function()
        {
          var windowHeight = $(window).height();
          var windowWidth = $(window).width();

          var number = (typeof this.modal.width === 'number');

          if (!number && this.modal.width.match(/%$/))
          {
            this.$modal.css({ 'width': this.modal.width, 'margin-bottom': '16px' });
          }
          else if (parseInt(this.modal.width) > windowWidth)
          {
            this.$modal.css({ 'width': '96%', 'margin-bottom': '2%' });
          }
          else
          {
            if (number)
            {
              this.modal.width += 'px';
            }

            this.$modal.css({ 'width': this.modal.width, 'margin-bottom': '16px' });
          }

          // margin top
          var height = this.$modal.outerHeight();
          var top = (windowHeight/2 - height/2) + 'px';

          if (this.detect.isMobile())
          {
            top = '2%';
          }
          else if (height > windowHeight)
          {
            top = '16px';

          }

          this.$modal.css('margin-top', top);
        },
        buildTabber: function()
        {
          this.modal.tabs = this.$modal.find('.redactor-modal-tab');

          if (this.modal.tabs.length < 2)
          {
            return;
          }

          this.modal.$tabsBox = $('<div id="redactor-modal-tabber" />');
          $.each(this.modal.tabs, $.proxy(function(i,s)
          {
            var a = $('<a href="#" rel="' + i + '" />').text($(s).attr('data-title'));

            a.on('click', $.proxy(this.modal.showTab, this));

            if (i === 0)
            {
              a.addClass('active');
            }

            this.modal.$tabsBox.append(a);

          }, this));

          this.$modalBody.prepend(this.modal.$tabsBox);

        },
        showTab: function(e)
        {
          e.preventDefault();

          var $el = $(e.target);
          var index = $el.attr('rel');

          this.modal.tabs.hide();
          this.modal.tabs.eq(index).show();

          $('#redactor-modal-tabber').find('a').removeClass('active');
          $el.addClass('active');

          return false;

        },
        setTitle: function(title)
        {
          this.$modalHeader.html(title);
        },
        setContent: function()
        {
          this.$modalBody.html(this.modal.getTemplate(this.modal.templateName));

          this.modal.getCancelButton().on('mousedown', $.proxy(this.modal.close, this));
        },
        setDraggable: function()
        {
          if (typeof $.fn.draggable === 'undefined')
          {
            return;
          }

          this.$modal.draggable({ handle: this.$modalHeader });
          this.$modalHeader.css('cursor', 'move');
        },
        setEnter: function(e)
        {
          if (e.which !== 13)
          {
            return;
          }

          e.preventDefault();
          this.modal.getActionButton().click();
        },
        build: function()
        {
          this.modal.buildOverlay();

          this.$modalBox = $('<div id="redactor-modal-box"/>').hide();
          this.$modal = $('<div id="redactor-modal" role="dialog" />');
          this.$modalHeader = $('<div id="redactor-modal-header" />');
          this.$modalClose = $('<button type="button" id="redactor-modal-close" aria-label="' + this.lang.get('close') + '" />').html('&times;');
          this.$modalBody = $('<div id="redactor-modal-body" />');

          this.$modal.append(this.$modalHeader);
          this.$modal.append(this.$modalBody);
          this.$modal.append(this.$modalClose);
          this.$modalBox.append(this.$modal);
          this.$modalBox.appendTo(document.body);

        },
        buildOverlay: function()
        {
          this.$modalOverlay = $('<div id="redactor-modal-overlay">').hide();
          $('body').prepend(this.$modalOverlay);
        },
        enableEvents: function()
        {
          this.$modalClose.on('mousedown.redactor-modal', $.proxy(this.modal.close, this));
          $(document).on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this));
          this.core.editor().on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this));
          this.$modalBox.on('click.redactor-modal', $.proxy(this.modal.close, this));
        },
        disableEvents: function()
        {
          this.$modalClose.off('mousedown.redactor-modal');
          $(document).off('keyup.redactor-modal');
          this.core.editor().off('keyup.redactor-modal');
          this.$modalBox.off('click§.redactor-modal');
          $(window).off('resize.redactor-modal');
        },
        closeHandler: function(e)
        {
          if (e.which !== this.keyCode.ESC)
          {
            return;
          }

          this.modal.close(false);
        },
        close: function(e)
        {
          if (e)
          {
            if ($(e.target).attr('id') !== 'redactor-modal-button-cancel' && e.target !== this.$modalClose[0] && e.target !== this.$modalBox[0])
            {
              return;
            }

            e.preventDefault();
          }

          if (!this.$modalBox)
          {
            return;
          }

          // restore selection
          this.selection.restore();

          this.modal.disableEvents();
          this.utils.enableBodyScroll();
          this.utils.restoreScroll();

          this.$modalOverlay.redactorAnimation('fadeOut', { duration: 0.4 }, $.proxy(function()
          {
            this.$modalOverlay.remove();

          }, this));

          this.$modal.redactorAnimation('fadeOut', {

            duration: 0.3,
            timing: 'cubic-bezier(0.175, 0.885, 0.320, 1.175)'

          }, $.proxy(function()
          {
            if (typeof this.$modalBox !== 'undefined')
            {
              this.$modalBox.remove();
              this.$modalBox = undefined;
            }

            this.core.callback('modalClosed', this.modal.templateName);

          }, this));

        }
      };
    },

    // =observe
    observe: function()
    {
      return {
        load: function()
        {
          if (typeof this.opts.destroyed !== 'undefined')
          {
            return;
          }

          this.observe.links();
          this.observe.images();

        },
        isCurrent: function($el, $current)
        {
          if (typeof $current === 'undefined')
          {
            $current = $(this.selection.current());
          }

          return $current.is($el) || $current.parents($el).length > 0;
        },
        toolbar: function()
        {
            this.observe.buttons();
            this.observe.dropdowns();
        },
        buttons: function(e, btnName)
        {
          var current = this.selection.current();
          var parent = this.selection.parent();

          if (e !== false) this.button.setInactiveAll();
          else             this.button.setInactiveAll(btnName);


          if (e === false && btnName !== 'html')
          {
            if ($.inArray(btnName, this.opts.activeButtons) !== -1) this.button.toggleActive(btnName);
            return;
          }

          if (!this.utils.isRedactorParent(current))
          {
            return;
          }

          // disable line
          if (this.core.editor().css('display') !== 'none')
          {
              if (this.utils.isCurrentOrParentHeader() || this.utils.isCurrentOrParent(['table', 'pre', 'blockquote', 'li'])) this.button.disable('horizontalrule');
              else this.button.enable('horizontalrule');
          }

          $.each(this.opts.activeButtonsStates, $.proxy(function(key, value)
          {
            var parentEl = $(parent).closest(key, this.$editor[0]);
            var currentEl = $(current).closest(key, this.$editor[0]);

            if (parentEl.length !== 0 && !this.utils.isRedactorParent(parentEl))
            {
              return;
            }

            if (!this.utils.isRedactorParent(currentEl))
            {
              return;
            }

            if (parentEl.length !== 0 || currentEl.closest(key, this.$editor[0]).length !== 0)
            {
              this.button.setActive(value);
            }

          }, this));

        },
        dropdowns: function()
        {
            var finded = $('<div />').html(this.selection.html()).find('a').length;
          var $current = $(this.selection.current());
          var isRedactor = this.utils.isRedactorParent($current);

          $.each(this.opts.observe.dropdowns, $.proxy(function(key, value)
          {
            var observe = value.observe,
              element = observe.element,
              $item   = value.item,
              inValues = typeof observe['in'] !== 'undefined' ? observe['in'] : false,
              outValues = typeof observe.out !== 'undefined' ? observe.out : false;

            if (($current.closest(element).length > 0 && isRedactor) || (element === 'a' && finded !== 0))
            {
              this.observe.setDropdownProperties($item, inValues, outValues);
            }
            else
            {
              this.observe.setDropdownProperties($item, outValues, inValues);
            }

          }, this));
        },
        setDropdownProperties: function($item, addProperties, deleteProperties)
        {
          if (deleteProperties && typeof deleteProperties.attr !== 'undefined')
          {
            this.observe.setDropdownAttr($item, deleteProperties.attr, true);
          }

          if (typeof addProperties.attr !== 'undefined')
          {
            this.observe.setDropdownAttr($item, addProperties.attr);
          }

          if (typeof addProperties.title !== 'undefined')
          {
            $item.find('span').text(addProperties.title);
          }
        },
        setDropdownAttr: function($item, properties, isDelete)
        {
          $.each(properties, function(key, value)
          {
            if (key === 'class')
            {
              if (!isDelete)
              {
                $item.addClass(value);
              }
              else
              {
                $item.removeClass(value);
              }
            }
            else
            {
              if (!isDelete)
              {
                $item.attr(key, value);
              }
              else
              {
                $item.removeAttr(key);
              }
            }
          });
        },
        addDropdown: function($item, btnName, btnObject)
        {
          if (typeof btnObject.observe === "undefined")
          {
            return;
          }

          btnObject.item = $item;

          this.opts.observe.dropdowns.push(btnObject);
        },
        images: function()
        {
                    if (this.opts.imageEditable)
                    {
                        this.core.editor().addClass('redactor-layer-img-edit');
              this.core.editor().find('img').each($.proxy(function(i, img)
              {
                var $img = $(img);

                // IE fix (when we clicked on an image and then press backspace IE does goes to image's url)
                $img.closest('a', this.$editor[0]).on('click', function(e) { e.preventDefault(); });

                this.image.setEditable($img);


              }, this));
                    }
        },
        links: function()
        {
          if (this.opts.linkTooltip)
          {
              this.core.editor().find('a').each($.proxy(function(i, s)
              {
                  var $link = $(s);
                  if ($link.data('cached') !== true)
                  {
                      $link.data('cached', true);
                      $link.on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.showTooltip, this));
                  }

              }, this));
          }
        },
        getTooltipPosition: function($link)
        {
          return $link.offset();
        },
        showTooltip: function(e)
        {
          var $el = $(e.target);

          if ($el[0].tagName === 'IMG')
          {
            return;
          }

          if ($el[0].tagName !== 'A')
          {
            $el = $el.closest('a', this.$editor[0]);
          }

          if ($el[0].tagName !== 'A')
          {
            return;
          }

          var $link = $el;

          var pos = this.observe.getTooltipPosition($link);
          var tooltip = $('<span class="redactor-link-tooltip"></span>');

          var href = $link.attr('href');
          if (href === undefined)
          {
            href = '';
          }

          if (href.length > 24)
          {
            href = href.substring(0, 24) + '...';
          }

          var aLink = $('<a href="' + $link.attr('href') + '" target="_blank" />').html(href).addClass('redactor-link-tooltip-action');
          var aEdit = $('<a href="#" />').html(this.lang.get('edit')).on('click', $.proxy(this.link.show, this)).addClass('redactor-link-tooltip-action');
          var aUnlink = $('<a href="#" />').html(this.lang.get('unlink')).on('click', $.proxy(this.link.unlink, this)).addClass('redactor-link-tooltip-action');

          tooltip.append(aLink).append(' | ').append(aEdit).append(' | ').append(aUnlink);

          var lineHeight = parseInt($link.css('line-height'), 10);
                    var lineClicked = Math.ceil((e.pageY - pos.top)/lineHeight);
                    var top = pos.top + lineClicked * lineHeight;

          tooltip.css({
            top: top + 'px',
            left: pos.left + 'px'
          });

          $('.redactor-link-tooltip').remove();
          $('body').append(tooltip);

          this.core.editor().on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this));
          $(document).on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this));
        },
        closeAllTooltip: function()
        {
          $('.redactor-link-tooltip').remove();
        },
        closeTooltip: function(e)
        {
          e = e.originalEvent || e;

          var target = e.target;
          var $parent = $(target).closest('a', this.$editor[0]);
          if ($parent.length !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A')
          {
            return;
          }
          else if ((target.tagName === 'A' && this.utils.isRedactorParent(target)) || $(target).hasClass('redactor-link-tooltip-action'))
          {
            return;
          }

          this.observe.closeAllTooltip();

          this.core.editor().off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this));
          $(document).off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this));
        }

      };
    },

    // =offset
    offset: function()
    {
      return {
        get: function(node)
        {
          var cloned = this.offset.clone(node);
          if (cloned === false)
          {
            return 0;
          }

          var div = document.createElement('div');
          div.appendChild(cloned.cloneContents());
          div.innerHTML = div.innerHTML.replace(/<img(.*?[^>])>$/gi, 'i');

              var text = $.trim($(div).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g, '');

          return text.length;

        },
        clone: function(node)
        {
          var sel = this.selection.get();
          var range = this.selection.range(sel);

          if (range === null && typeof node === 'undefined')
          {
            return false;
          }

          node = (typeof node === 'undefined') ? this.$editor : node;
          if (node === false)
          {
            return false;
          }

          node = node[0] || node;

          var cloned = range.cloneRange();
          cloned.selectNodeContents(node);
          cloned.setEnd(range.endContainer, range.endOffset);

          return cloned;
        },
        set: function(start, end)
        {
          end = (typeof end === 'undefined') ? start : end;

          if (!this.focus.is())
          {
            this.focus.start();
          }

          var sel = this.selection.get();
          var range = this.selection.range(sel);
          var node, offset = 0;
          var walker = document.createTreeWalker(this.$editor[0], NodeFilter.SHOW_TEXT, null, null);

          while ((node = walker.nextNode()) !== null)
          {
            offset += node.nodeValue.length;
            if (offset > start)
            {
              range.setStart(node, node.nodeValue.length + start - offset);
              start = Infinity;
            }

            if (offset >= end)
            {
              range.setEnd(node, node.nodeValue.length + end - offset);
              break;
            }
          }

          range.collapse(false);
          this.selection.update(sel, range);
        }
      };
    },

    // =paragraphize
    paragraphize: function()
    {
      return {
        load: function(html)
        {
          if (this.opts.paragraphize === false || this.opts.type === 'inline' || this.opts.type === 'pre')
          {
            return html;
          }

          if (html === '' || html === '<p></p>')
          {
            return this.opts.emptyHtml;
          }

          html = html + "\n";

          this.paragraphize.safes = [];
          this.paragraphize.z = 0;

          // before
          html = html.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>');
          html = html.replace(/<\/pre>/gi, "</pre>\n\n");
          html = html.replace(/<p>\s<br><\/p>/gi, '<p></p>');

          html = this.paragraphize.getSafes(html);

          html = html.replace('<br>', "\n");
          html = this.paragraphize.convert(html);

          html = this.paragraphize.clear(html);
          html = this.paragraphize.restoreSafes(html);

          // after
          html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts.paragraphizeBlocks.join('|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');

          return $.trim(html);
        },
        getSafes: function(html)
        {
          var $div = $('<div />').append(html);

          // remove paragraphs in blockquotes
          $div.find('blockquote p').replaceWith(function()
          {
            return $(this).append('<br />').contents();
          });

                    $div.find(this.opts.paragraphizeBlocks.join(', ')).each($.proxy(function(i,s)
          {
            this.paragraphize.z++;
            this.paragraphize.safes[this.paragraphize.z] = s.outerHTML;

            return $(s).replaceWith("\n#####replace" + this.paragraphize.z + "#####\n\n");


          }, this));

                    // deal with redactor selection markers
                    $div.find('span.redactor-selection-marker').each($.proxy(function(i,s)
                    {
                        this.paragraphize.z++;
                        this.paragraphize.safes[this.paragraphize.z] = s.outerHTML;

                        return $(s).replaceWith("\n#####replace" + this.paragraphize.z + "#####\n\n");
                    }, this));

          return $div.html();
        },
        restoreSafes: function(html)
        {
          $.each(this.paragraphize.safes, function(i,s)
          {
            s = (typeof s !== 'undefined') ? s.replace(/\$/g, '&#36;') : s;
            html = html.replace('#####replace' + i + '#####', s);

          });

          return html;
        },
        convert: function(html)
        {
          html = html.replace(/\r\n/g, "xparagraphmarkerz");
          html = html.replace(/\n/g, "xparagraphmarkerz");
          html = html.replace(/\r/g, "xparagraphmarkerz");

          var re1 = /\s+/g;
          html = html.replace(re1, " ");
          html = $.trim(html);

          var re2 = /xparagraphmarkerzxparagraphmarkerz/gi;
          html = html.replace(re2, "</p><p>");

          var re3 = /xparagraphmarkerz/gi;
          html = html.replace(re3, "<br>");

          html = '<p>' + html + '</p>';

          html = html.replace("<p></p>", "");
          html = html.replace("\r\n\r\n", "");
          html = html.replace(/<\/p><p>/g, "</p>\r\n\r\n<p>");
          html = html.replace(new RegExp("<br\\s?/?></p>", "g"), "</p>");
          html = html.replace(new RegExp("<p><br\\s?/?>", "g"), "<p>");
          html = html.replace(new RegExp("<p><br\\s?/?>", "g"), "<p>");
          html = html.replace(new RegExp("<br\\s?/?></p>", "g"), "</p>");
          html = html.replace(/<p>&nbsp;<\/p>/gi, "");
          html = html.replace(/<p>\s?<br>&nbsp;<\/p>/gi, '');
          html = html.replace(/<p>\s?<br>/gi, '<p>');

          return html;
        },
        clear: function(html)
        {

          html = html.replace(/<p>(.*?)#####replace(.*?)#####\s?<\/p>/gi, '<p>$1</p>#####replace$2#####');
          html = html.replace(/(<br\s?\/?>){2,}<\/p>/gi, '</p>');

          html = html.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>');
          html = html.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>');
          html = html.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>');
          html = html.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>');

          html = html.replace(new RegExp('<p><p ', 'gi'), '<p ');
          html = html.replace(new RegExp('<p><p>', 'gi'), '<p>');
          html = html.replace(new RegExp('</p></p>', 'gi'), '</p>');
          html = html.replace(new RegExp('<p>\\s?</p>', 'gi'), '');
          html = html.replace(new RegExp("\n</p>", 'gi'), '</p>');
          html = html.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>');
          html = html.replace(new RegExp('<p>\t*</p>', 'gi'), '');

          return html;
        }
      };
    },

    // =paste
    paste: function()
    {
      return {
        init: function(e)
        {
          this.rtePaste = true;
          var pre = (this.opts.type === 'pre' || this.utils.isCurrentOrParent('pre')) ? true : false;

          // clipboard event
          if (this.detect.isDesktop())
          {

              if (!this.paste.pre && this.opts.clipboardImageUpload && this.opts.imageUpload && this.paste.detectClipboardUpload(e))
              {
                if (this.detect.isIe())
                {
                  setTimeout($.proxy(this.paste.clipboardUpload, this), 100);
                }

                return;
              }
          }

          this.utils.saveScroll();
          this.selection.save();
          this.paste.createPasteBox(pre);

          $(window).on('scroll.redactor-freeze', $.proxy(function()
          {
            $(window).scrollTop(this.saveBodyScroll);

          }, this));

          setTimeout($.proxy(function()
          {
            var html = this.paste.getPasteBoxCode(pre);

            // buffer
            this.buffer.set();
            this.selection.restore();

            this.utils.restoreScroll();

            // paste info
            var data = this.clean.getCurrentType(html);

            // clean
            html = this.clean.onPaste(html, data);

            // callback
            var returned = this.core.callback('paste', html);
            html = (typeof returned === 'undefined') ? html : returned;

            this.paste.insert(html, data);
            this.rtePaste = false;

            // clean pre breaklines
            if (pre)
            {
              this.clean.cleanPre();
            }

            $(window).off('scroll.redactor-freeze');

          }, this), 1);

        },
        getPasteBoxCode: function(pre)
        {
          var html = (pre) ? this.$pasteBox.val() : this.$pasteBox.html();
          this.$pasteBox.remove();

          return html;
        },
        createPasteBox: function(pre)
        {
          var css = { position: 'fixed', width: '1px', top: 0, left: '-9999px' };

          this.$pasteBox = (pre) ? $('<textarea>').css(css) : $('<div>').attr('contenteditable', 'true').css(css);
          this.paste.appendPasteBox();
          this.$pasteBox.focus();
        },
        appendPasteBox: function()
        {
          if (this.detect.isIe())
          {
            this.core.box().append(this.$pasteBox);
          }
          else
          {
            // bootstrap modal
            var $visibleModals = $('.modal-body:visible');
            if ($visibleModals.length > 0)
            {
              $visibleModals.append(this.$pasteBox);
            }
            else
            {
              $('body').prepend(this.$pasteBox);
            }
          }
        },
        detectClipboardUpload: function(e)
        {
          e = e.originalEvent || e;

          var clipboard = e.clipboardData;
          if (this.detect.isIe() || this.detect.isFirefox())
          {
            return false;
          }

          // prevent safari fake url
          var types = clipboard.types;
          if (types.indexOf('public.tiff') !== -1)
          {
            e.preventDefault();
            return false;
          }

          if (!clipboard.items || !clipboard.items.length)
          {
            return;
          }

          var file = clipboard.items[0].getAsFile();
          if (file === null)
          {
            return false;
          }

          var reader = new FileReader();
          reader.readAsDataURL(file);
          reader.onload = $.proxy(this.paste.insertFromClipboard, this);

          return true;
        },
        clipboardUpload: function()
        {
          var imgs = this.$editor.find('img');
          $.each(imgs, $.proxy(function(i,s)
          {
            if (s.src.search(/^data\:image/i) === -1)
            {
              return;
            }

            var formData = !!window.FormData ? new FormData() : null;
            if (!window.FormData)
            {
              return;
            }

            this.upload.direct = true;
            this.upload.type = 'image';
            this.upload.url = this.opts.imageUpload;
            this.upload.callback = $.proxy(function(data)
            {
              if (this.detect.isIe())
              {
                $(s).wrap($('<figure />'));
              }

              else
              {
                var $parent = $(s).parent();
                this.utils.replaceToTag($parent, 'figure');
              }

              s.src = data.url;
              this.core.callback('imageUpload', $(s), data);

            }, this);

            var blob = this.utils.dataURItoBlob(s.src);

            formData.append('clipboard', 1);
            formData.append(this.opts.imageUploadParam, blob);

            this.progress.show();
            this.upload.send(formData, false);
            this.code.sync();
            this.rtePaste = false;

          }, this));
        },
        insertFromClipboard: function(e)
        {
          var formData = !!window.FormData ? new FormData() : null;
          if (!window.FormData)
          {
            return;
          }

          this.upload.direct = true;
          this.upload.type = 'image';
          this.upload.url = this.opts.imageUpload;
          this.upload.callback = this.image.insert;

          var blob = this.utils.dataURItoBlob(e.target.result);

          formData.append('clipboard', 1);
          formData.append(this.opts.imageUploadParam, blob);

          this.progress.show();
          this.upload.send(formData, e);
          this.rtePaste = false;
        },
        insert: function(html, data)
        {
          if (data.pre)
          {
            this.insert.raw(html);
          }
          else if (data.text)
          {
            this.insert.text(html);
          }
          else
          {
            this.insert.html(html, data);
          }

          // Firefox Clipboard Observe
          if (this.detect.isFirefox() && this.opts.clipboardImageUpload)
          {
            setTimeout($.proxy(this.paste.clipboardUpload, this), 100);
          }

        }
      };
    },

    // =placeholder
    placeholder: function()
    {
      return {

        // public
        enable: function()
        {
          setTimeout($.proxy(function()
          {
            return (this.placeholder.isEditorEmpty()) ? this.placeholder.show() : this.placeholder.hide();

          }, this), 5);
        },
        show: function()
        {
          this.core.editor().addClass('redactor-placeholder');
        },
        update: function(text)
        {
          this.opts.placeholder = text;
          this.core.editor().attr('placeholder', text);
        },
        hide: function()
        {
          this.core.editor().removeClass('redactor-placeholder');
        },
        is: function()
        {
          return this.core.editor().hasClass('redactor-placeholder');
        },


        // private
        init: function()
        {
          if (!this.placeholder.enabled())
          {
            return;
          }

          if (!this.utils.isEditorRelative())
          {
            this.utils.setEditorRelative();
          }

          this.placeholder.build();
          this.placeholder.buildPosition();
          this.placeholder.enable();
          this.placeholder.enableEvents();

        },
        enabled: function()
        {
          return (this.opts.placeholder) ? this.core.element().attr('placeholder', this.opts.placeholder) : this.placeholder.isAttr();
        },
        enableEvents: function()
        {
          this.core.editor().on('keydown.redactor-placeholder.' + this.uuid, $.proxy(this.placeholder.enable, this));
        },
        disableEvents: function()
        {
          this.core.editor().off('.redactor-placeholder.' + this.uuid);
        },
        build: function()
        {
          this.core.editor().attr('placeholder', this.core.element().attr('placeholder'));
        },
        buildPosition: function()
        {
          var $style = $('<style />');
          $style.addClass('redactor-placeholder-style-tag');
          $style.html('#' + this.core.id() + '.redactor-placeholder::after ' + this.placeholder.getPosition());

          $('head').append($style);
        },
        getPosition: function()
        {
          return '{ top: ' + this.core.editor().css('padding-top') + '; left: ' + this.core.editor().css('padding-left') + '; }';
        },
        isEditorEmpty: function()
        {
          var html = $.trim(this.core.editor().html()).replace(/[\t\n]/g, '');
          var states = ['', '<p>​</p>', '<p>​<br></p>', this.opts.emptyHtmlRendered];

          return ($.inArray(html, states) !== -1);
        },
        isAttr: function()
        {
          return (typeof this.core.element().attr('placeholder') !== 'undefined' && this.core.element().attr('placeholder') !== '');
        },
        destroy: function()
        {
          this.core.editor().removeAttr('placeholder');

          this.placeholder.hide();
          this.placeholder.disableEvents();

          $('.redactor-placeholder-style-tag').remove();
        }
      };
    },

    // =progress
    progress: function()
    {
      return {
        $box: null,
        $bar: null,
        target: document.body,  // or id selector

        // public
        show: function()
        {
          if (!this.progress.is())
          {
            this.progress.build();
            this.progress.$box.redactorAnimation('fadeIn');
          }
          else
          {
            this.progress.$box.show();
          }
        },
        hide: function()
        {
          if (this.progress.is())
          {
            this.progress.$box.redactorAnimation('fadeOut', { duration: 0.35 }, $.proxy(this.progress.destroy, this));
          }
        },
        update: function(value)
        {
          this.progress.show();
          this.progress.$bar.css('width', value + '%');
        },
        is: function()
        {
          return (this.progress.$box === null) ? false : true;
        },

        // private
        build: function()
        {
          this.progress.$bar = $('<span />');
          this.progress.$box = $('<div id="redactor-progress" />');

          this.progress.$box.append(this.progress.$bar);
          $(this.progress.target).append(this.progress.$box);
        },
        destroy: function()
        {
          if (this.progress.is())
          {
            this.progress.$box.remove();
          }

          this.progress.$box = null;
          this.progress.$bar = null;
        }
      };
    },

    // =selection
    selection: function()
    {
      return {
        get: function()
        {
          if (window.getSelection)
          {
            return window.getSelection();
          }
          else if (document.selection && document.selection.type !== "Control")
          {
            return document.selection;
          }

          return null;
        },
        range: function(sel)
        {
          if (typeof sel === 'undefined')
          {
            sel = this.selection.get();
          }

          if (sel.getRangeAt && sel.rangeCount)
          {
            return sel.getRangeAt(0);
          }

          return null;
        },
        is: function()
        {
          return (this.selection.isCollapsed()) ? false : true;
        },
        isRedactor: function()
        {
          var range = this.selection.range();

          if (range !== null)
          {
            var el = range.startContainer.parentNode;

            if ($(el).hasClass('redactor-in') || $(el).parents('.redactor-in').length !== 0)
            {
              return true;
            }
          }

          return false;
        },
        isCollapsed: function()
        {
          var sel = this.selection.get();

          return (sel === null) ? false : sel.isCollapsed;
        },
        update: function(sel, range)
        {
          if (range === null)
          {
            return;
          }

          sel.removeAllRanges();
          sel.addRange(range);
        },
        current: function()
        {
          var sel = this.selection.get();

          return (sel === null) ? false : sel.anchorNode;
        },
        parent: function()
        {
          var current = this.selection.current();

          return (current === null) ? false : current.parentNode;
        },
        block: function(node)
        {
          node = node || this.selection.current();

          while (node)
          {
            if (this.utils.isBlockTag(node.tagName))
            {
              return ($(node).hasClass('redactor-in')) ? false : node;
            }

            node = node.parentNode;
          }

          return false;
        },
        inline: function(node)
        {
          node = node || this.selection.current();

          while (node)
          {
            if (this.utils.isInlineTag(node.tagName))
            {
              return ($(node).hasClass('redactor-in')) ? false : node;
            }

            node = node.parentNode;
          }

          return false;
        },
        element: function(node)
        {
          if (!node)
          {
            node = this.selection.current();
          }

          while (node)
          {
            if (node.nodeType === 1)
            {
              if ($(node).hasClass('redactor-in'))
              {
                return false;
              }

              return node;
            }

            node = node.parentNode;
          }

          return false;
        },
        prev: function()
        {
          var current = this.selection.current();

          return (current === null) ? false : this.selection.current().previousSibling;
        },
        next: function()
        {
          var current = this.selection.current();

          return (current === null) ? false : this.selection.current().nextSibling;
        },
        blocks: function(tag)
        {
          var blocks = [];
          var nodes = this.selection.nodes(tag);

          $.each(nodes, $.proxy(function(i,node)
          {
            if (this.utils.isBlock(node))
            {
              blocks.push(node);
            }

          }, this));

          var block = this.selection.block();
          if (blocks.length === 0 && block === false)
          {
            return [];
          }
          else if (blocks.length === 0 && block !== false)
          {
            return [block];
          }
          else
          {
            return blocks;
          }

        },
        inlines: function(tag)
        {
          var inlines = [];
          var nodes = this.selection.nodes(tag);

          $.each(nodes, $.proxy(function(i,node)
          {
            if (this.utils.isInline(node))
            {
              inlines.push(node);
            }

          }, this));

          var inline = this.selection.inline();
          if (inlines.length === 0 && inline === false)
          {
            return [];
          }
          else if (inlines.length === 0 && inline !== false)
          {
            return [inline];
          }
          else
          {
            return inlines;
          }
        },
        nodes: function(tag)
        {
                    var filter = (typeof tag === 'undefined') ? [] : (($.isArray(tag)) ? tag : [tag]);

          var sel = this.selection.get();
          var range = this.selection.range(sel);
                    var nodes = [];
                    var resultNodes = [];

          if (this.utils.isCollapsed())
          {
            nodes = [this.selection.current()];
          }
          else
          {
            var node = range.startContainer;
            var endNode = range.endContainer;

            // single node
            if (node === endNode)
            {
              return [this.selection.parent()];
            }

            // iterate
            while (node && node !== endNode)
            {
              nodes.push(node = this.selection.nextNode(node));
            }

            // partially selected nodes
            node = range.startContainer;
            while (node && node !== range.commonAncestorContainer)
            {
              nodes.unshift(node);
              node = node.parentNode;
            }
          }

          // remove service nodes
          $.each(nodes, function(i,s)
          {
            if (s)
            {
              var tagName = (s.nodeType !== 1) ? false : s.tagName.toLowerCase();

              if ($(s).hasClass('redactor-script-tag') || $(s).hasClass('redactor-selection-marker'))
              {
                return;
              }
              else if (tagName && filter.length !== 0 && $.inArray(tagName, filter) === -1)
              {
                return;
              }
              else
              {
                resultNodes.push(s);
              }
            }
          });

          return (resultNodes.length === 0) ? [] : resultNodes;
        },
        nextNode: function(node)
        {
          if (node.hasChildNodes())
          {
            return node.firstChild;
          }
          else
          {
            while (node && !node.nextSibling)
            {
              node = node.parentNode;
            }

            if (!node)
            {
              return null;
            }

            return node.nextSibling;
          }
        },
        save: function()
        {
          this.marker.insert();
          this.savedSel = this.core.editor().html();
        },
        restore: function(removeMarkers)
        {
                    var node1 = this.marker.find(1);
          var node2 = this.marker.find(2);

          if (this.detect.isFirefox())
          {
            this.core.editor().focus();
          }

          if (node1.length !== 0 && node2.length !== 0)
          {
            this.caret.set(node1, node2);
          }
          else if (node1.length !== 0)
          {
            this.caret.start(node1);
          }
          else
          {
            this.core.editor().focus();
          }

          if (removeMarkers !== false)
          {
            this.marker.remove();
            this.savedSel = false;
          }
        },
                saveInstant: function()
                {
                    var el = this.core.editor()[0];
                    var doc = el.ownerDocument, win = doc.defaultView;
                    var sel = win.getSelection();

                    if (!sel.getRangeAt || !sel.rangeCount)
                    {
                        return;
                    }

                    var range = sel.getRangeAt(0);
                    var selectionRange = range.cloneRange();

                    selectionRange.selectNodeContents(el);
                    selectionRange.setEnd(range.startContainer, range.startOffset);

                    var start = selectionRange.toString().length;

                    this.saved = {
                        start: start,
                        end: start + range.toString().length,
                        node: range.startContainer
                    };

                    return this.saved;
                },
                restoreInstant: function(saved)
                {
                    if (typeof saved === 'undefined' && !this.saved)
                    {
                        return;
                    }

                    this.saved = (typeof saved !== 'undefined') ? saved : this.saved;

                    var $node = this.core.editor().find(this.saved.node);
                    if ($node.length !== 0 && $node.text().trim().replace(/\u200B/g, '').length === 0)
                    {
                       try {
                            var range = document.createRange();
                            range.setStart($node[0], 0);

                            var sel = window.getSelection();
                            sel.removeAllRanges();
                            sel.addRange(range);
                        }
                        catch(e) {}

                        return;
                    }

                    var el = this.core.editor()[0];
                    var doc = el.ownerDocument, win = doc.defaultView;
                    var charIndex = 0, range = doc.createRange();

                    range.setStart(el, 0);
                    range.collapse(true);

                    var nodeStack = [el], node, foundStart = false, stop = false;
                    while (!stop && (node = nodeStack.pop()))
                    {
                        if (node.nodeType == 3)
                        {
                            var nextCharIndex = charIndex + node.length;
                            if (!foundStart && this.saved.start >= charIndex && this.saved.start <= nextCharIndex)
                            {
                                range.setStart(node, this.saved.start - charIndex);
                                foundStart = true;
                            }

                            if (foundStart && this.saved.end >= charIndex && this.saved.end <= nextCharIndex)
                            {
                                range.setEnd(node, this.saved.end - charIndex);
                                stop = true;
                            }
                            charIndex = nextCharIndex;
                        }
                        else
                        {
                            var i = node.childNodes.length;
                            while (i--)
                            {
                                nodeStack.push(node.childNodes[i]);
                            }
                        }
                    }

                    var sel = win.getSelection();
                    sel.removeAllRanges();
                    sel.addRange(range);
                },
        node: function(node)
        {
          $(node).prepend(this.marker.get(1));
          $(node).append(this.marker.get(2));

          this.selection.restore();
        },
        all: function()
        {
          this.core.editor().focus();

          var sel = this.selection.get();
          var range = this.selection.range(sel);

          range.selectNodeContents(this.core.editor()[0]);

          this.selection.update(sel, range);
        },
        remove: function()
        {
          this.selection.get().removeAllRanges();
        },
        replace: function(html)
        {
          this.insert.html(html);
        },
        text: function()
        {
          return this.selection.get().toString();
        },
        html: function()
        {
          var html = '';
          var sel = this.selection.get();

          if (sel.rangeCount)
          {
            var container = document.createElement('div');
            var len = sel.rangeCount;
            for (var i = 0; i < len; ++i)
            {
              container.appendChild(sel.getRangeAt(i).cloneContents());
            }

            html = this.clean.onGet(container.innerHTML);
          }

          return html;
        },
        extractEndOfNode: function(node)
        {
          var sel = this.selection.get();
          var range = this.selection.range(sel);

              var clonedRange = range.cloneRange();
              clonedRange.selectNodeContents(node);
              clonedRange.setStart(range.endContainer, range.endOffset);

              return clonedRange.extractContents();
        },

        // #backward
        removeMarkers: function()
        {
          this.marker.remove();
        },
        marker: function(num)
        {
          return this.marker.get(num);
        },
        markerHtml: function(num)
        {
          return this.marker.html(num);
        }

      };
    },

    // =shortcuts
    shortcuts: function()
    {
      return {
        // based on https://github.com/jeresig/jquery.hotkeys
        hotkeysSpecialKeys: {
          8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
          20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
          37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=",
          96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
          104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
          112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
          120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=",
          188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'"
        },
        hotkeysShiftNums: {
          "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
          "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
          ".": ">",  "/": "?",  "\\": "|"
        },
        init: function(e, key)
        {
          // disable browser's hot keys for bold and italic if shortcuts off
          if (this.opts.shortcuts === false)
          {
            if ((e.ctrlKey || e.metaKey) && (key === 66 || key === 73))
            {
              e.preventDefault();
            }

            return false;
          }
          else
          {
            // build
            $.each(this.opts.shortcuts, $.proxy(function(str, command)
            {
              this.shortcuts.build(e, str, command);

            }, this));
          }
        },
        build: function(e, str, command)
        {
          var handler = $.proxy(function()
          {
            this.shortcuts.buildHandler(command);

          }, this);

          var keys = str.split(',');
          var len = keys.length;
          for (var i = 0; i < len; i++)
          {
            if (typeof keys[i] === 'string')
            {
              this.shortcuts.handler(e, $.trim(keys[i]), handler);
            }
          }

        },
        buildHandler: function(command)
        {
          var func;
          if (command.func.search(/\./) !== '-1')
          {
            func = command.func.split('.');
            if (typeof this[func[0]] !== 'undefined')
            {
              this[func[0]][func[1]].apply(this, command.params);
            }
          }
          else
          {
            this[command.func].apply(this, command.params);
          }
        },
        handler: function(e, keys, origHandler)
        {
          keys = keys.toLowerCase().split(" ");

          var special = this.shortcuts.hotkeysSpecialKeys[e.keyCode];
          var character = String.fromCharCode(e.which).toLowerCase();
          var modif = "", possible = {};

          $.each([ "alt", "ctrl", "meta", "shift"], function(index, specialKey)
          {
            if (e[specialKey + 'Key'] && special !== specialKey)
            {
              modif += specialKey + '+';
            }
          });


          if (special)
          {
            possible[modif + special] = true;
          }

          if (character)
          {
            possible[modif + character] = true;
            possible[modif + this.shortcuts.hotkeysShiftNums[character]] = true;

            // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
            if (modif === "shift+")
            {
              possible[this.shortcuts.hotkeysShiftNums[character]] = true;
            }
          }

          var len = keys.length;
          for (var i = 0; i < len; i++)
          {
            if (possible[keys[i]])
            {
              e.preventDefault();
              return origHandler.apply(this, arguments);
            }
          }
        }
      };
    },

    // =storage
    storage: function()
    {
      return {
        data: [],
        add: function(data)
        {
          // type, node, url, id
          data.status = true;
          data.url = decodeURI(data.url);

          this.storage.data[data.url] = data;
        },
        status: function(url, status)
        {
          this.storage.data[decodeURI(url)].status = status;
        },
        observe: function()
        {
          var _this = this;

          var $images = this.core.editor().find('[data-image]');
          $images.each(function(i, s)
          {
            _this.storage.add({ type: 'image', node: s, url: s.src, id: $(s).attr('data-image') });
          });

          var $files = this.core.editor().find('[data-file]');
          $files.each(function(i, s)
          {
            _this.storage.add({ type: 'file', node: s, url: s.href, id: $(s).attr('data-file') });
          });

          var $s3 = this.core.editor().find('[data-s3]');
          $s3.each(function(i, s)
          {
            var url = (s.tagName === 'IMG') ? s.src : s.href;
            _this.storage.add({ type: 's3', node: s, url: url, id: $(s).attr('data-s3') });
          });

        },
        changes: function()
        {
          for (var key in this.storage.data)
          {
            var data = this.storage.data[key];
            var attr = (data.node.tagName === 'IMG') ? 'src' : 'href';
            var $el = this.core.editor().find('[data-' + data.type + '][' + attr + '="' + data.url + '"]');

            if ($el.length === 0)
            {
              this.storage.status(data.url, false);
            }
            else
            {
              this.storage.status(data.url, true);
            }
          }


          return this.storage.data;
        }

      };
    },

    // =toolbar
    toolbar: function()
    {
      return {
        build: function()
        {
          this.button.hideButtons();
          this.button.hideButtonsOnMobile();

          this.$toolbar = this.toolbar.createContainer();

          this.toolbar.append();
          this.button.$toolbar = this.$toolbar;
          this.button.setFormatting();
          this.button.load(this.$toolbar);

          this.toolbar.setOverflow();
          this.toolbar.setFixed();

        },
        createContainer: function()
        {
          return $('<ul>').addClass('redactor-toolbar').attr({ 'id': 'redactor-toolbar-' + this.uuid, 'role': 'toolbar' });
        },
        append: function()
        {
          if (this.opts.toolbarExternal)
          {
            this.$toolbar.addClass('redactor-toolbar-external');
            $(this.opts.toolbarExternal).html(this.$toolbar);
          }
          else
          {
            if (this.opts.type === 'textarea')
            {
              this.$box.prepend(this.$toolbar);
            }
            else
            {
              this.$element.before(this.$toolbar);
            }

          }
        },
        setOverflow: function()
        {
            if (this.opts.toolbarOverflow)
          {
              this.$toolbar.addClass('redactor-toolbar-overflow');
            }
        },
        setFixed: function()
        {
          if (!this.opts.toolbarFixed || this.opts.toolbarExternal)
          {
            return;
          }

          if (this.opts.toolbarFixedTarget !== document)
          {
            var $el = $(this.opts.toolbarFixedTarget);
            this.toolbar.toolbarOffsetTop = ($el.length === 0) ? 0 : this.core.box().offset().top - $el.offset().top;
          }

          // bootstrap modal fix
          var late = (this.core.box().closest('.modal-body').length !== 0) ? 1000 : 0;

          setTimeout($.proxy(function()
          {
              var self = this;
            this.toolbar.observeScroll(false);
            if (this.detect.isDesktop())
            {
              $(this.opts.toolbarFixedTarget).on('scroll.redactor.' + this.uuid, function()
              {
                                if (self.core.editor().height() < 100 || self.placeholder.isEditorEmpty())
                      {
                          return;
                      }

                  self.toolbar.observeScroll();
                            });
            }
            else
            {
              $(this.opts.toolbarFixedTarget).on('scroll.redactor.' + this.uuid, function()
              {
                                if (self.core.editor().height() < 100 || self.placeholder.isEditorEmpty())
                      {
                          return;
                      }

                self.core.toolbar().hide();
                clearTimeout($.data(this, "scrollCheck"));
                $.data(this, "scrollCheck", setTimeout(function()
                {
                  self.core.toolbar().show();
                  self.toolbar.observeScroll();
                }, 250) );

              });
            }

          }, this), late);

        },
        setUnfixed: function()
        {
            this.toolbar.observeScrollDisable();
        },
        getBoxTop: function()
        {
          return (this.opts.toolbarFixedTarget === document) ? this.core.box().offset().top : this.toolbar.toolbarOffsetTop;
        },
        observeScroll: function(start)
        {
          // tolerance 0 if redactor in the hidden layer
          var tolerance = 0;

          if (start !== false)
          {
            tolerance = (this.opts.toolbarFixedTarget === document) ? 20 : 0;
          }

          var scrollTop = $(this.opts.toolbarFixedTarget).scrollTop();
          var boxTop = this.toolbar.getBoxTop();

                    if (scrollTop === boxTop)
                    {
                        return;
                    }

          if ((scrollTop + this.opts.toolbarFixedTopOffset + tolerance) > boxTop)
          {
            this.toolbar.observeScrollEnable(scrollTop, boxTop);
          }
          else
          {
            this.toolbar.observeScrollDisable();
          }
        },
        observeScrollResize: function()
        {
          this.$toolbar.css({

            width: this.core.box().innerWidth(),
            left: this.core.box().offset().left

          });
        },
        observeScrollEnable: function(scrollTop, boxTop)
        {
          if (typeof this.fullscreen !== 'undefined' && this.fullscreen.isOpened === false)
          {
            this.toolbar.observeScrollDisable();
            return;
          }

          var end = boxTop + this.core.box().outerHeight() - 32;
          var width = this.core.box().innerWidth();

          var position = (this.detect.isDesktop()) ? 'fixed' : 'absolute';
          var top = (this.detect.isDesktop()) ? this.opts.toolbarFixedTopOffset : ($(this.opts.toolbarFixedTarget).scrollTop() - boxTop + this.opts.toolbarFixedTopOffset);
          var left = (this.detect.isDesktop()) ? this.core.box().offset().left : 0;

          if (this.opts.toolbarFixedTarget !== document)
          {
             position = 'absolute';
             top = this.opts.toolbarFixedTopOffset + $(this.opts.toolbarFixedTarget).scrollTop() - boxTop;
             left = 0;
          }

          this.$toolbar.addClass('toolbar-fixed-box');

          this.$toolbar.css({
            position: position,
            width: width,
            top: top,
            left: left
          });


          if (scrollTop > end)
          {
            $('.redactor-dropdown-' + this.uuid + ':visible').hide();
          }

          this.toolbar.setDropdownsFixed();

          this.$toolbar.css('visibility', (scrollTop < end) ? 'visible' : 'hidden');
          $(window).on('resize.redactor-toolbar.' + this.uuid, $.proxy(this.toolbar.observeScrollResize, this));
        },
        observeScrollDisable: function()
        {
          this.$toolbar.css({
            position: 'relative',
            width: 'auto',
            top: 0,
            left: 0,
            visibility: 'visible'
          });

          this.toolbar.unsetDropdownsFixed();
          this.$toolbar.removeClass('toolbar-fixed-box');
          $(window).off('resize.redactor-toolbar.' + this.uuid);
        },
        setDropdownsFixed: function()
        {
          var position = (this.opts.toolbarFixedTarget === document && this.detect.isDesktop()) ? 'fixed' : 'absolute';
          this.toolbar.setDropdownPosition(position);
        },
        unsetDropdownsFixed: function()
        {
          this.toolbar.setDropdownPosition('absolute');
        },
        setDropdownPosition: function(position)
        {
          var self = this;
          $('.redactor-dropdown-' + this.uuid).each(function()
          {
            var $el = $(this);
            var $button = self.button.get($el.attr('rel'));
            var top = (position === 'fixed') ? self.opts.toolbarFixedTopOffset : $button.offset().top;

            $el.css({ position: position, top: ($button.innerHeight() + top) + 'px' });
          });
        }
      };
    },

    // =upload
    upload: function()
    {
      return {
        init: function(id, url, callback)
        {
          this.upload.direct = false;
          this.upload.callback = callback;
          this.upload.url = url;
          this.upload.$el = $(id);
          this.upload.$droparea = $('<div id="redactor-droparea" />');

          this.upload.$placeholdler = $('<div id="redactor-droparea-placeholder" />').text(this.lang.get('upload-label'));
          this.upload.$input = $('<input type="file" name="file" multiple />');

          this.upload.$placeholdler.append(this.upload.$input);
          this.upload.$droparea.append(this.upload.$placeholdler);
          this.upload.$el.append(this.upload.$droparea);

          this.upload.$droparea.off('redactor.upload');
          this.upload.$input.off('redactor.upload');

          this.upload.$droparea.on('dragover.redactor.upload', $.proxy(this.upload.onDrag, this));
          this.upload.$droparea.on('dragleave.redactor.upload', $.proxy(this.upload.onDragLeave, this));

          // change
          this.upload.$input.on('change.redactor.upload', $.proxy(function(e)
          {
                        e = e.originalEvent || e;
                        var len = this.upload.$input[0].files.length;

                        for (var i = 0; i < len; i++)
                        {
                            var index = (len - 1) - i;
                            this.upload.traverseFile(this.upload.$input[0].files[index], e);
                        }
          }, this));

          // drop
          this.upload.$droparea.on('drop.redactor.upload', $.proxy(function(e)
          {
            e.preventDefault();

            this.upload.$droparea.removeClass('drag-hover').addClass('drag-drop');
            this.upload.onDrop(e);

          }, this));
        },
        directUpload: function(file, e)
        {
          this.upload.direct = true;
          this.upload.traverseFile(file, e);
        },
        onDrop: function(e)
        {
          e = e.originalEvent || e;
          var files = e.dataTransfer.files;

          if (this.opts.multipleImageUpload)
          {
            var len = files.length;
            for (var i = 0; i < len; i++)
            {
              this.upload.traverseFile(files[i], e);
            }
          }
          else
          {
            this.upload.traverseFile(files[0], e);
          }
        },
        traverseFile: function(file, e)
        {
          if (this.opts.s3)
          {
            this.upload.setConfig(file);
            this.uploads3.send(file, e);
            return;
          }

          var formData = !!window.FormData ? new FormData() : null;
          if (window.FormData)
          {
            this.upload.setConfig(file);

            var name = (this.upload.type === 'image') ? this.opts.imageUploadParam : this.opts.fileUploadParam;
            formData.append(name, file);
          }

          this.progress.show();
          var stop = this.core.callback('uploadStart', e, formData);
          if (stop !== false)
          {
              this.upload.send(formData, e);
          }
        },
        setConfig: function(file)
        {
          this.upload.getType(file);

          if (this.upload.direct)
          {
            this.upload.url = (this.upload.type === 'image') ? this.opts.imageUpload : this.opts.fileUpload;
            this.upload.callback = (this.upload.type === 'image') ? this.image.insert : this.file.insert;
          }
        },
        getType: function(file)
        {
          this.upload.type = (this.opts.imageTypes.indexOf(file.type) === -1) ? 'file' : 'image';

          if (this.opts.imageUpload === null && this.opts.fileUpload !== null)
          {
            this.upload.type = 'file';
          }
        },
        getHiddenFields: function(obj, fd)
        {
          if (obj === false || typeof obj !== 'object')
          {
            return fd;
          }

          $.each(obj, $.proxy(function(k, v)
          {
            if (v !== null && v.toString().indexOf('#') === 0)
            {
              v = $(v).val();
            }

            fd.append(k, v);

          }, this));

          return fd;

        },
        send: function(formData, e)
        {
          // append hidden fields
          if (this.upload.type === 'image')
          {
            formData = this.utils.appendFields(this.opts.imageUploadFields, formData);
            formData = this.utils.appendForms(this.opts.imageUploadForms, formData);
            formData = this.upload.getHiddenFields(this.upload.imageFields, formData);
          }
          else
          {
            formData = this.utils.appendFields(this.opts.fileUploadFields, formData);
            formData = this.utils.appendForms(this.opts.fileUploadForms, formData);
            formData = this.upload.getHiddenFields(this.upload.fileFields, formData);
          }

          var xhr = new XMLHttpRequest();
          xhr.open('POST', this.upload.url);
          xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

          // complete
          xhr.onreadystatechange = $.proxy(function()
          {
              if (xhr.readyState === 4)
              {
                  var data = xhr.responseText;

              data = data.replace(/^\[/, '');
              data = data.replace(/\]$/, '');

              var json;
              try
              {
                json = (typeof data === 'string' ? JSON.parse(data) : data);
              }
              catch(err)
              {
                json = { error: true };
              }


              this.progress.hide();

              if (!this.upload.direct)
              {
                this.upload.$droparea.removeClass('drag-drop');
              }

              this.upload.callback(json, this.upload.direct, e);
              }
          }, this);

                    // before send
                    var stop = this.core.callback('uploadBeforeSend', xhr);
                    if (stop !== false)
                    {
              xhr.send(formData);
          }
        },
        onDrag: function(e)
        {
          e.preventDefault();
          this.upload.$droparea.addClass('drag-hover');
        },
        onDragLeave: function(e)
        {
          e.preventDefault();
          this.upload.$droparea.removeClass('drag-hover');
        },
        clearImageFields: function()
        {
          this.upload.imageFields = {};
        },
        addImageFields: function(name, value)
        {
          this.upload.imageFields[name] = value;
        },
        removeImageFields: function(name)
        {
          delete this.upload.imageFields[name];
        },
        clearFileFields: function()
        {
          this.upload.fileFields = {};
        },
        addFileFields: function(name, value)
        {
          this.upload.fileFields[name] = value;
        },
        removeFileFields: function(name)
        {
          delete this.upload.fileFields[name];
        }
      };
    },

    // =s3
    uploads3: function()
    {
      return {
        send: function(file, e)
        {
          this.uploads3.executeOnSignedUrl(file, $.proxy(function(signedURL)
          {
            this.uploads3.sendToS3(file, signedURL, e);
          }, this));
        },
        executeOnSignedUrl: function(file, callback)
        {
          var xhr = new XMLHttpRequest();
          var mark = (this.opts.s3.search(/\?/) === -1) ? '?' : '&';

          xhr.open('GET', this.opts.s3 + mark + 'name=' + file.name + '&type=' + file.type, true);

          // hack to pass bytes through unprocessed.
          if (xhr.overrideMimeType)
          {
            xhr.overrideMimeType('text/plain; charset=x-user-defined');
          }

          var that = this;
          xhr.onreadystatechange = function(e)
          {
            if (this.readyState === 4 && this.status === 200)
            {
              that.progress.show();
              callback(decodeURIComponent(this.responseText));
            }
          };

          xhr.send();
        },
        createCORSRequest: function(method, url)
        {
          var xhr = new XMLHttpRequest();
          if ("withCredentials" in xhr)
          {
            xhr.open(method, url, true);
          }
          else if (typeof XDomainRequest !== "undefined")
          {
            xhr = new XDomainRequest();
            xhr.open(method, url);
          }
          else
          {
            xhr = null;
          }

          return xhr;
        },

                sendToS3: function(file, url, e)
                {
                    var xhr = this.uploads3.createCORSRequest('PUT', url);
                    if (!xhr)
                    {
                        return;
                    }

                    xhr.onload = $.proxy(function()
                    {
                        var json;
                        this.progress.hide();

                        if (xhr.status !== 200)
                        {
                            // error
                            json = { error: true };
                            this.upload.callback(json, this.upload.direct, xhr);

                            return;
                        }

                        var s3file = url.split('?');
                        if (!s3file[0])
                        {
                            // url parsing is fail
                            return false;
                        }


                        if (!this.upload.direct)
                        {
                            this.upload.$droparea.removeClass('drag-drop');
                        }

                        json = { url: s3file[0], id: s3file[0], s3: true };
                        if (this.upload.type === 'file')
                        {
                            var arr = s3file[0].split('/');
                            json.name = arr[arr.length-1];
                        }

                        this.upload.callback(json, this.upload.direct, e);


                    }, this);

                    xhr.onerror = function() {};
                    xhr.upload.onprogress = function(e) {};

                    xhr.setRequestHeader('Content-Type', file.type);
                    xhr.setRequestHeader('x-amz-acl', 'public-read');

                    xhr.send(file);

                }
      };
    },

    // =utils
    utils: function()
    {
      return {
        isEmpty: function(html)
        {
          html = (typeof html === 'undefined') ? this.core.editor().html() : html;

          html = html.replace(/[\u200B-\u200D\uFEFF]/g, '');
          html = html.replace(/&nbsp;/gi, '');
          html = html.replace(/<\/?br\s?\/?>/g, '');
          html = html.replace(/\s/g, '');
          html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
          html = html.replace(/<iframe(.*?[^>])>$/i, 'iframe');
          html = html.replace(/<source(.*?[^>])>$/i, 'source');

          // remove empty tags
          html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
          html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');

          html = $.trim(html);

          return html === '';
        },
        isElement: function(obj)
        {
          try {
            // Using W3 DOM2 (works for FF, Opera and Chrome)
            return obj instanceof HTMLElement;
          }
          catch(e)
          {
            return (typeof obj === "object") && (obj.nodeType === 1) && (typeof obj.style === "object") && (typeof obj.ownerDocument === "object");
          }
        },
        strpos: function(haystack, needle, offset)
        {
          var i = haystack.indexOf(needle, offset);
          return i >= 0 ? i : false;
        },
        dataURItoBlob: function(dataURI)
        {
          var byteString;
          if (dataURI.split(',')[0].indexOf('base64') >= 0)
          {
            byteString = atob(dataURI.split(',')[1]);
          }
          else
          {
            byteString = unescape(dataURI.split(',')[1]);
          }

          var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

          var ia = new Uint8Array(byteString.length);
          for (var i = 0; i < byteString.length; i++)
          {
            ia[i] = byteString.charCodeAt(i);
          }

          return new Blob([ia], { type:mimeString });
        },
        getOuterHtml: function(el)
        {
          return $('<div>').append($(el).eq(0).clone()).html();
        },
        cloneAttributes: function(from, to)
        {
          from = from[0] || from;
          to = $(to);

          var attrs = from.attributes;
          var len = attrs.length;
          while (len--)
          {
              var attr = attrs[len];
              to.attr(attr.name, attr.value);
          }

          return to;
        },
        breakBlockTag: function()
        {
          var block = this.selection.block();
          if (!block)
          {
              return false;
          }

          var isEmpty = this.utils.isEmpty(block.innerHTML);

          var tag = block.tagName.toLowerCase();
          if (tag === 'pre' || tag === 'li' || tag === 'td' || tag === 'th')
          {
            return false;
          }


          if (!isEmpty && this.utils.isStartOfElement(block))
          {
            return { $block: $(block), $next: $(block).next(), type: 'start' };
          }
          else if (!isEmpty && this.utils.isEndOfElement(block))
          {
            return { $block: $(block), $next: $(block).next(), type: 'end' };
          }
          else
          {
            var endOfNode = this.selection.extractEndOfNode(block);
            var $nextPart = $('<' + tag + ' />').append(endOfNode);

            $nextPart = this.utils.cloneAttributes(block, $nextPart);
            $(block).after($nextPart);

            return { $block: $(block), $next: $nextPart, type: 'break' };
          }
        },
        // tag detection
        inBlocks: function(tags)
        {
          tags = ($.isArray(tags)) ? tags : [tags];

          var blocks = this.selection.blocks();
          var len = blocks.length;
          var contains = false;
          for (var i = 0; i < len; i++)
          {
            if (blocks[i] !== false)
            {
              var tag = blocks[i].tagName.toLowerCase();

              if ($.inArray(tag, tags) !== -1)
              {
                contains = true;
              }
            }
          }

          return contains;

        },
        inInlines: function(tags)
        {
          tags = ($.isArray(tags)) ? tags : [tags];

          var inlines = this.selection.inlines();
          var len = inlines.length;
          var contains = false;
          for (var i = 0; i < len; i++)
          {
            var tag = inlines[i].tagName.toLowerCase();

            if ($.inArray(tag, tags) !== -1)
            {
              contains = true;
            }
          }

          return contains;

        },
        isTag: function(current, tag)
        {
          var element = $(current).closest(tag, this.core.editor()[0]);
          if (element.length === 1)
          {
            return element[0];
          }

          return false;
        },
        isBlock: function(block)
        {
          if (block === null)
          {
            return false;
          }

          block = block[0] || block;

          return block && this.utils.isBlockTag(block.tagName);
        },
        isBlockTag: function(tag)
        {
          return (typeof tag === 'undefined') ? false : this.reIsBlock.test(tag);
        },
        isInline: function(inline)
        {
          inline = inline[0] || inline;

          return inline && this.utils.isInlineTag(inline.tagName);
        },
        isInlineTag: function(tag)
        {
          return (typeof tag === 'undefined') ? false : this.reIsInline.test(tag);
        },
        // parents detection
        isRedactorParent: function(el)
        {
          if (!el)
          {
            return false;
          }

          if ($(el).parents('.redactor-in').length === 0 || $(el).hasClass('redactor-in'))
          {
            return false;
          }

          return el;
        },
        isCurrentOrParentHeader: function()
        {
          return this.utils.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
        },
        isCurrentOrParent: function(tagName)
        {
          var parent = this.selection.parent();
          var current = this.selection.current();

          if ($.isArray(tagName))
          {
            var matched = 0;
            $.each(tagName, $.proxy(function(i, s)
            {
              if (this.utils.isCurrentOrParentOne(current, parent, s))
              {
                matched++;
              }
            }, this));

            return (matched === 0) ? false : true;
          }
          else
          {
            return this.utils.isCurrentOrParentOne(current, parent, tagName);
          }
        },
        isCurrentOrParentOne: function(current, parent, tagName)
        {
          tagName = tagName.toUpperCase();

          return parent && parent.tagName === tagName ? parent : current && current.tagName === tagName ? current : false;
        },
        isEditorRelative: function()
        {
          var position = this.core.editor().css('position');
          var arr = ['absolute', 'fixed', 'relative'];

          return ($.inArray(arr, position) !== -1);
        },
        setEditorRelative: function()
        {
          this.core.editor().addClass('redactor-relative');
        },
        // scroll
        getScrollTarget: function()
        {
            var $scrollTarget = $(this.opts.scrollTarget);

            return ($scrollTarget.length !== 0) ? $scrollTarget : $(document);
        },
        freezeScroll: function()
        {
          this.freezeScrollTop = this.utils.getScrollTarget().scrollTop();
          this.utils.getScrollTarget().scrollTop(this.freezeScrollTop);
        },
        unfreezeScroll: function()
        {
          if (typeof this.freezeScrollTop === 'undefined')
          {
            return;
          }

          this.utils.getScrollTarget().scrollTop(this.freezeScrollTop);
        },
        saveScroll: function()
        {
          this.tmpScrollTop = this.utils.getScrollTarget().scrollTop();
        },
        restoreScroll: function()
        {
          if (typeof this.tmpScrollTop === 'undefined')
          {
            return;
          }

          this.utils.getScrollTarget().scrollTop(this.tmpScrollTop);
        },
        isStartOfElement: function(element)
        {
          if (typeof element === 'undefined')
          {
            element = this.selection.block();
            if (!element)
            {
              return false;
            }
          }

          return (this.offset.get(element) === 0) ? true : false;
        },
        isEndOfElement: function(element)
        {
          if (typeof element === 'undefined')
          {
            element = this.selection.block();
            if (!element)
            {
              return false;
            }
          }

          var text = $.trim($(element).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g, '');
          var offset = this.offset.get(element);

          return (offset === text.length) ? true : false;
        },
        removeEmptyAttr: function(el, attr)
        {
          var $el = $(el);
          if (typeof $el.attr(attr) === 'undefined')
          {
            return true;
          }

          if ($el.attr(attr) === '')
          {
            $el.removeAttr(attr);
            return true;
          }

          return false;
        },
        replaceToTag: function(node, tag)
        {
          var replacement;
          $(node).replaceWith(function()
          {
            replacement = $('<' + tag + ' />').append($(this).contents());

            for (var i = 0; i < this.attributes.length; i++)
            {
              replacement.attr(this.attributes[i].name, this.attributes[i].value);
            }

            return replacement;
          });

          return replacement;
        },
        // select all
        isSelectAll: function()
        {
          return this.selectAll;
        },
        enableSelectAll: function()
        {
          this.selectAll = true;
        },
        disableSelectAll: function()
        {
          this.selectAll = false;
        },
        disableBodyScroll: function()
        {
          var $body = $('html');
          var windowWidth = window.innerWidth;
          if (!windowWidth)
          {
            var documentElementRect = document.documentElement.getBoundingClientRect();
            windowWidth = documentElementRect.right - Math.abs(documentElementRect.left);
          }

          var isOverflowing = document.body.clientWidth < windowWidth;
          var scrollbarWidth = this.utils.measureScrollbar();

          $body.css('overflow', 'hidden');
          if (isOverflowing)
          {
            $body.css('padding-right', scrollbarWidth);
          }
        },
        measureScrollbar: function()
        {
          var $body = $('body');
          var scrollDiv = document.createElement('div');
          scrollDiv.className = 'redactor-scrollbar-measure';

          $body.append(scrollDiv);
          var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
          $body[0].removeChild(scrollDiv);
          return scrollbarWidth;
        },
        enableBodyScroll: function()
        {
          $('html').css({ 'overflow': '', 'padding-right': '' });
          $('body').remove('redactor-scrollbar-measure');
        },
        appendFields: function(appendFields, data)
        {
          if (!appendFields)
          {
            return data;
          }
          else if (typeof appendFields === 'object')
          {
              $.each(appendFields, function(k, v)
              {
                if (v !== null && v.toString().indexOf('#') === 0)
                {
                    v = $(v).val();
                            }

                data.append(k, v);

              });

              return data;
          }

          var $fields = $(appendFields);
          if ($fields.length === 0)
          {
            return data;
          }
          else
          {
            var str = '';
            $fields.each(function()
            {
              data.append($(this).attr('name'), $(this).val());
            });

            return data;
          }
        },
        appendForms: function(appendForms, data)
        {
          if (!appendForms)
          {
            return data;
          }

          var $forms = $(appendForms);
          if ($forms.length === 0)
          {
            return data;
          }
          else
          {
            var formData = $forms.serializeArray();

            $.each(formData, function(z,f)
            {
              data.append(f.name, f.value);
            });

            return data;
          }
        },
        isRgb: function(str)
        {
                    return (str.search(/^rgb/i) === 0);
        },
        rgb2hex: function(rgb)
        {
                    rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);

                    return (rgb && rgb.length === 4) ? "#" +
                    ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
                    ("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
                    ("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
                },

        // #backward
        isCollapsed: function()
        {
          return this.selection.isCollapsed();
        },
        isMobile: function()
        {
          return this.detect.isMobile();
        },
        isDesktop: function()
        {
          return this.detect.isDesktop();
        },
        isPad: function()
        {
          return this.detect.isIpad();
        }

      };
    },

    // #backward
    browser: function()
    {
      return {
        webkit: function()
        {
          return this.detect.isWebkit();
        },
        ff: function()
        {
          return this.detect.isFirefox();
        },
        ie: function()
        {
          return this.detect.isIe();
        }
      };
    }
  };

  $(window).on('load.tools.redactor', function()
  {
    $('[data-tools="redactor"]').redactor();
  });

  // constructor
  Redactor.prototype.init.prototype = Redactor.prototype;

})(jQuery);

(function($)
{
  $.fn.redactorAnimation = function(animation, options, callback)
  {
    return this.each(function()
    {
      new redactorAnimation(this, animation, options, callback);
    });
  };

  function redactorAnimation(element, animation, options, callback)
  {
    // default
    var opts = {
      duration: 0.5,
      iterate: 1,
      delay: 0,
      prefix: 'redactor-',
      timing: 'linear'
    };

    this.animation = animation;
    this.slide = (this.animation === 'slideDown' || this.animation === 'slideUp');
    this.$element = $(element);
    this.prefixes = ['', '-moz-', '-o-animation-', '-webkit-'];
    this.queue = [];

    // options or callback
    if (typeof options === 'function')
    {
      callback = options;
      this.opts = opts;
    }
    else
    {
      this.opts = $.extend(opts, options);
    }

    // slide
    if (this.slide)
    {
      this.$element.height(this.$element.height());
    }

    // init
    this.init(callback);

  }

  redactorAnimation.prototype = {

    init: function(callback)
    {
      this.queue.push(this.animation);

      this.clean();

      if (this.animation === 'show')
      {
        this.opts.timing = 'linear';
        this.$element.removeClass('hide').show();

        if (typeof callback === 'function')
        {
          callback(this);
        }
      }
      else if (this.animation === 'hide')
      {
        this.opts.timing = 'linear';
        this.$element.hide();

        if (typeof callback === 'function')
        {
          callback(this);
        }
      }
      else
      {
        this.animate(callback);
      }

    },
    animate: function(callback)
    {
      this.$element.addClass('redactor-animated').css('display', '').removeClass('hide');
      this.$element.addClass(this.opts.prefix + this.queue[0]);

      this.set(this.opts.duration + 's', this.opts.delay + 's', this.opts.iterate, this.opts.timing);

      var _callback = (this.queue.length > 1) ? null : callback;
      this.complete('AnimationEnd', $.proxy(function()
      {
        if (this.$element.hasClass(this.opts.prefix + this.queue[0]))
        {
          this.clean();
          this.queue.shift();

          if (this.queue.length)
          {
            this.animate(callback);
          }
        }

      }, this), _callback);
    },
    set: function(duration, delay, iterate, timing)
    {
      var len = this.prefixes.length;

      while (len--)
      {
        this.$element.css(this.prefixes[len] + 'animation-duration', duration);
        this.$element.css(this.prefixes[len] + 'animation-delay', delay);
        this.$element.css(this.prefixes[len] + 'animation-iteration-count', iterate);
        this.$element.css(this.prefixes[len] + 'animation-timing-function', timing);
      }

    },
    clean: function()
    {
      this.$element.removeClass('redactor-animated');
      this.$element.removeClass(this.opts.prefix + this.queue[0]);

      this.set('', '', '', '');

    },
    complete: function(type, make, callback)
    {
      this.$element.one(type.toLowerCase() + ' webkit' + type + ' o' + type + ' MS' + type, $.proxy(function()
      {
        if (typeof make === 'function')
        {
          make();
        }

        if (typeof callback === 'function')
        {
          callback(this);
        }

        // hide
        var effects = ['fadeOut', 'slideUp', 'zoomOut', 'slideOutUp', 'slideOutRight', 'slideOutLeft'];
        if ($.inArray(this.animation, effects) !== -1)
        {
          this.$element.css('display', 'none');
        }

        // slide
        if (this.slide)
        {
          this.$element.css('height', '');
        }

      }, this));

    }
  };

})(jQuery);
(function () {
    'use strict';

    angular.module('app', [
        'app.core',
        'app.auth',
        'app.widgets',
        'app.admin',
        'app.services',
        'app.dashboard',
        'app.stations',
        'app.users',
        'app.layout'
    ]);

})();

(function () {
    'use strict';

    angular.module('app.admin', [
        'app.core',
        'app.widgets'
      ]);

})();

(function () {
    'use strict';

    angular
        .module('blocks.analytics', [
        ]);
})();

(function() {
    'use strict';

    angular.module('blocks.exception', ['blocks.logger']);
})();

(function() {
    'use strict';

    var mod = angular.module('blocks.logger', [])
})();

(function() {
    'use strict';

    angular.module('blocks.router', [
        'ui.router',
        'blocks.logger'
    ]);
})();

(function () {
    'use strict';


    var core = angular
        .module('app.core', [
            'ngAnimate', 'ngSanitize', 'ngStorage', 'ngResource',
            'blocks.exception', 'blocks.logger', 'blocks.router', 'blocks.analytics',
            'ui.router', 'ngplus', 'ngFileUpload', 'countrySelect',
            'pascalprecht.translate', 'infinite-scroll', 'ngInflection', 'angularMoment', 'ui.bootstrap'
        ]);
    core.run(appRun);

    appRun.$inject = ['$rootScope', 'amMoment', '$translate', '$templateCache', '$timeout'];
    function appRun($rootScope, amMoment, $translate, $templateCache, $timeout) {
        amMoment.changeLocale($translate.use());

        $rootScope.$on('$includeContentLoaded', redactor);
        // redactor();

        function redactor(ev) {
            $rootScope.$applyAsync(function() {
                $timeout(function() {
                    $(':not(.redactor-box) > .redactor').redactor({
                        buttons: ['bold', 'italic', 'underline', 'deleted', 'lists', 'link'],
                        lang: 'fr',
                        // focus: false,
                        // toolbarOverflow: true,
                        air: true,
                        placeholder: 'Écrivez ce que vous voulez et sélectionnez du texte pour le formater.'
                    });
                }, 50);
            });
        }

    }
})();

(function() {
    'use strict';

	var mod = angular.module('app.dashboard', [
      'app',
      'daterangepicker'
    ]);

})();

(function() {
    'use strict';

    angular.module('app.layout', ['app.core']);
})();

(function() {
    'use strict';

    var services = angular.module('app.services', [
        'app',
	    'app.auth'
      ]);
})();

(function () {
    'use strict';

    var auth = angular
        .module('app.auth', [
          'app.core'
        ]);

    auth.run(appRun);


    appRun.$inject = ['$rootScope', '$q', '$state', 'session', 'SignInApi', '$location', '$localStorage'];
    function appRun($rootScope, $q, $state, session, SignInApi, $location, $localStorage) {
      $rootScope.$on('unauthorized', function(e, currentState) {
        console.log('auth', 'unauthorized', e, currentState);
        $localStorage.redirectAfterSignInPath = currentState ? currentState.name : '/';
        $state.go('auth.signOut');
      });
    }

})();

(function() {
    'use strict';

    var stations = angular.module('app.stations', [
        'app.core',
        'app.widgets'
      ]);
})();

(function() {
    'use strict';

    var users = angular.module('app.users', [
        'app.core',
        'app.widgets',
        'ngHeatmap'
      ]);
})();

(function() {
    'use strict';

    angular.module('app.widgets', []);
})();

(function () {
    'use strict';

    angular
        .module('app.admin')
        .controller('AdminController', AdminController);

    AdminController.$inject = ['logger'];
    /*  */
    function AdminController(logger) {
        var vm = this;
        vm.title = 'Admin';

        activate();

        function activate() {
            // logger.info('Activated Admin View');
        }
    }
})();

(function() {
    'use strict';

    angular
        .module('app.admin')
        .run(appRun);

    appRun.$inject = ['routerHelper'];
    /*  */
    function appRun(routerHelper) {
        routerHelper.configureStates(getStates());
    }

    function getStates() {
        return [
            {
                state: 'admin',
                config: {
                    url: '/admin',
                    templateUrl: 'app/admin/admin.html',
                    controller: 'AdminController',
                    controllerAs: 'vm',
                    title: 'Admin',
                    settings: {
                        nav: null, // 2
                        level: ['admin'],
                        content: '<i class="fa fa-lock"></i> <span class="smartphone-hidden">Admin</span>'
                    }
                }
            }
        ];
    }
})();

(function () {
    'use strict';

    angular
        .module('app')
        .controller('AppController', AppController);

    AppController.$inject = ['$rootScope', '$scope', '$http', '$localStorage', '$state', '$interval', 'myservice'];
    function AppController($rootScope, $scope, $http, $localStorage, $state, $interval, myservice) {
        var vm = this;

        $interval(
          function(event) {
            // env vars
            $http.get('/api/config.json').then(function(content) {
              var config = angular.fromJson(content.data);
              window.TRACKING_KEY = config.TRACKING_KEY;
              window.APPLICATION_ID = config.APPLICATION_ID;
              window.API_KEY = config.API_KEY;
              window.DC_DOMAIN = config.DC_DOMAIN;
              window.DEV_DC_DOMAIN = config.DEV_DC_DOMAIN;
              window.DASHBOARD_SOURCE = config.DASHBOARD_SOURCE;
              if (config.FRONTEND_VERSION > FRONTEND_VERSION) {
                // $window.location.reload();
                $(document).trigger('reload-needed');
              } else if (config.FRONTEND_VERSION < FRONTEND_VERSION) {
                $(document).trigger('rollback-needed');
              }
            });

            // redactor
            // redactor();
          }, 30*1000);

        // check localstorage
        myservice.getMe();
    }
})();

// Include in index.html so that app level exceptions are handled.
// Exclude from testRunner.html which should run exactly what it wants to run
(function() {
    'use strict';

    angular
        .module('blocks.exception')
        .provider('exceptionHandler', exceptionHandlerProvider)
        .config(config);

    /**
     * Must configure the exception handling
     */
    function exceptionHandlerProvider() {
        /* jshint validthis:true */
        this.config = {
            appErrorPrefix: undefined
        };

        this.configure = function (appErrorPrefix) {
            this.config.appErrorPrefix = appErrorPrefix;
        };

        this.$get = function() {
            return {config: this.config};
        };
    }

    config.$inject = ['$provide'];

    /**
     * Configure by setting an optional string value for appErrorPrefix.
     * Accessible via config.appErrorPrefix (via config value).
     * @param  {Object} $provide
     */
    /*  */
    function config($provide) {
        $provide.decorator('$exceptionHandler', extendExceptionHandler);
    }

    extendExceptionHandler.$inject = ['$delegate', 'exceptionHandler', 'logger'];

    /**
     * Extend the $exceptionHandler service to also display a toast.
     * @param  {Object} $delegate
     * @param  {Object} exceptionHandler
     * @param  {Object} logger
     * @return {Function} the decorated $exceptionHandler service
     */
    function extendExceptionHandler($delegate, exceptionHandler, logger) {
        return function(exception, cause) {
            var appErrorPrefix = exceptionHandler.config.appErrorPrefix || '';
            var errorData = {exception: exception, cause: cause};
            if (exception.message) {
                exception.message = appErrorPrefix + exception.message;
                $delegate(exception, cause);
                /**
                 * Could add the error to a service's collection,
                 * add errors to $rootScope, log errors to remote web server,
                 * or log locally. Or throw hard. It is entirely up to you.
                 * throw exception;
                 *
                 * @example
                 *     throw { message: 'error message we added' };
                 */
                // logger.error(exception.message, errorData);
                console.log(exception, errorData);
            }
        };
    }
})();

(function() {
    'use strict';

    angular
        .module('blocks.exception')
        .factory('exception', exception);

    exception.$inject = ['$q', 'logger'];
    /*  */
    function exception($q, logger) {
        var service = {
            catcher: catcher
        };
        return service;

        function catcher(message) {
            return function(e) {
                var thrownDescription;
                var newMessage;
                if (e.data && e.data.description) {
                    thrownDescription = '\n' + e.data.description;
                    newMessage = message + thrownDescription;
                    e.data.description = newMessage;
                    logger.error(newMessage);
                }
                return $q.reject(e);
            };
        }
    }
})();

(function() {
    'use strict';

    angular
        .module('blocks.logger')
        .factory('logger', logger);

    logger.$inject = ['$log', 'toastr'];

    /*  */
    function logger($log, toastr) {
        var service = {
            showToasts: true,

            error   : error,
            info    : info,
            success : success,
            warning : warning,

            // straight to console; bypass toastr
            log     : $log.log
        };

        return service;
        /////////////////////

        function error(message, data, title) {
            // toastr.error(message, title);
            console.log('Error: ' + message, data);
            $log.error('Error: ' + message);
        }

        function info(message, data, title) {
            toastr.info(message, title);
            $log.info('Info: ' + message, data);
        }

        function success(message, data, title) {
            toastr.success(message, title);
            $log.info('Success: ' + message, data);
        }

        function warning(message, data, title) {
            toastr.warning(message, title);
            $log.warn('Warning: ' + message, data);
        }
    }
}());

/* Help configure the state-base ui.router */
(function() {
    'use strict';

    angular
        .module('blocks.router')
        .provider('routerHelper', routerHelperProvider);

    routerHelperProvider.$inject = ['$locationProvider', '$stateProvider', '$urlRouterProvider'];
    /*  */
    function routerHelperProvider($locationProvider, $stateProvider, $urlRouterProvider) {
        /* jshint validthis:true */
        var config = {
            docTitle: null,
            resolveAlways: {}
        };

        $locationProvider.html5Mode(true);

        this.configure = function(cfg) {
            angular.extend(config, cfg);
        };

        this.$get = RouterHelper;
        RouterHelper.$inject = ['$location', '$rootScope', '$state', 'logger'];
        /*  */
        function RouterHelper($location, $rootScope, $state, logger) {
            var handlingStateChangeError = false;
            var hasOtherwise = false;
            var stateCounts = {
                errors: 0,
                changes: 0
            };

            var service = {
                configureStates: configureStates,
                getStates: getStates,
                stateCounts: stateCounts
            };

            init();

            return service;

            ///////////////

            function configureStates(states, otherwisePath) {
                states.forEach(function(state) {
                    state.config.resolve =
                        angular.extend(state.config.resolve || {}, config.resolveAlways);
                    $stateProvider.state(state.state, state.config);
                });
                if (otherwisePath && !hasOtherwise) {
                    hasOtherwise = true;
                    $urlRouterProvider.otherwise(otherwisePath);
                }
            }

            function handleRoutingErrors() {
                // Route cancellation:
                // On routing error, go to the dashboard.
                // Provide an exit clause if it tries to do it twice.
                $rootScope.$on('$stateChangeError',
                    function(event, toState, toParams, fromState, fromParams, error) {
                        if (handlingStateChangeError) {
                            return;
                        }
                        stateCounts.errors++;
                        handlingStateChangeError = true;
                        var destination = (toState &&
                            (toState.title || toState.name || toState.loadedTemplateUrl)) ||
                            'unknown target';
                        var msg = 'Error routing to ' + destination + '. ' +
                            (error.data ? Object.keys(error.data).map(function(k,v) {return k+': '+error.data[k];}) : '') + '. <br/>' + (error.statusText || '') +
                            ': ' + (error.status || '');
                        logger.warning(msg, [toState]);
                        $urlRouterProvider.otherwise('/');
                    }
                );
            }

            function init() {
                handleRoutingErrors();
                updateDocTitle();
            }

            function getStates() { return $state.get(); }

            function updateDocTitle() {
                $rootScope.$on('$stateChangeSuccess',
                    function(event, toState, toParams, fromState, fromParams) {
                        stateCounts.changes++;
                        handlingStateChangeError = false;
                        var title = config.docTitle + ' ' + (toState.title || '');
                        $rootScope.title = title; // data bind to <title>
                    }
                );
            }
        }
    }
})();

(function () {
    'use strict';

    var core = angular.module('app.core');

    core.config(toastrConfig);

    toastrConfig.$inject = ['toastr'];
    /*  */
    function toastrConfig(toastr) {
        toastr.options.timeOut = 15000;
        toastr.options.positionClass = 'toast-top-right';
        toastr.options.newestOnTop = true;
        toastr.options.closeButton = true;
    }

    var configParams = {
        appErrorPrefix: '[Kiosque Divercities Error] ',
        appTitle: 'Kiosque Divercities'
    };

    core.value('config', configParams);

    core.factory('session', session).config(configure);

    configure.$inject = ['$logProvider', 'routerHelperProvider', 'exceptionHandlerProvider', '$translateProvider', '$httpProvider', '$sceDelegateProvider'];
    /*  */
    function configure($logProvider, routerHelperProvider, exceptionHandlerProvider, $translateProvider, $httpProvider, $sceDelegateProvider) {
        if ($logProvider.debugEnabled) {
            $logProvider.debugEnabled(true);
        }
        exceptionHandlerProvider.configure(configParams.appErrorPrefix);
        routerHelperProvider.configure({docTitle: configParams.appTitle + ': '});

        $translateProvider.useSanitizeValueStrategy(null);


        $httpProvider.defaults.useXDomain = true;
        $httpProvider.defaults.headers.common["Content-Type"] = "application/json";
        $httpProvider.defaults.headers.common['Accept'] = 'application/json';
        $sceDelegateProvider.resourceUrlWhitelist([
            // Allow same origin resource loads.
            'self',
            'https://*.divercities.eu/**',
            'https://*.1dtouch.com/**',
            'https://*.*.1dtouch.com/**',
            'https://*.*.divercities.eu/**',
            'https://*.*.*.divercities.eu/**',
            'https://*.google.com/**',
            'https://*.googleapis.com/**'
        ]);
    }



    session.$inject = ['$localStorage', '$window'];
    function session($localStorage, $window) {
        return {
            config: {
                authHeader: 'Authorization',
                authToken: 'Bearer'
            },
            isAuthenticated: function () {
                var token;

                if (!!$localStorage && !!$localStorage.user) {
                    token = $localStorage.user.token;
                }

                // A token is present
                if (token) {
                    // Token with a valid JWT format XXX.YYY.ZZZ
                    if (token.split('.').length === 3) {
                        // Could be a valid JWT or an access token with the same format
                        try {
                            var base64Url = token.split('.')[1];
                            var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
                            var exp = JSON.parse($window.atob(base64)).exp;
                            // JWT with an optonal expiration claims
                            if (exp) {
                                var isExpired = Math.round(new Date().getTime() / 1000) >= exp;
                                if (isExpired) {
                                    // FAIL: Expired token
                                    delete $localStorage.user.token;
                                    return false;
                                } else {
                                    // PASS: Non-expired token
                                    return true;
                                }
                            }
                        } catch(e) {
                            // PASS: Non-JWT token that looks like JWT
                            return true;
                        }
                    }
                    // PASS: All other tokens
                    return true;
                }
                // FAIL: No token at all
                return false;
            }
        };
    }

})();

/* global toastr:false, moment:false */
(function() {
    'use strict';

    angular
        .module('app.core')
        .constant('toastr', toastr)
        .constant('moment', moment);
})();

/*
 * Exemplo:
 * var users = [
 *   {name: 'Victor Queiroz'},
 *   {name: 'João Bosco'},
 *   {name: 'Ruan Jordão'}
 * ];
 *
 * Aplicando o filtro:
 * {{ users | pluck:'name' | join:', ' }}
 *
 * Retorno:
 * Victor Queiroz, João Bosco, Ruan Jordão
 * 
 * Dependências:
 * - VanillaJS
 */

(function () {
  'use strict';

  var core = angular.module('app.core');

  core
   .filter('notInByProp', function() {
     return function(objects, source, property) {
        if (!(objects && property && angular.isArray(objects))) return [];
        
        property = property ? String(property) : 'id';
        
       var ids = pluck(source, property);

       return objects.filter(function(v) {
         property = String(property);
         v = Object(v);

         return (v.hasOwnProperty(property) && ids.indexOf(v[property])==-1);
       });
     }
   })
    
   .filter('join', function() {
     return function(list, token) {
         return (list||[]).join(token);
     }
   })
    
   .filter('pluck', function() {
     return function(objects, property) {
         return pluck(objects, property);
     }
   })

   .filter('trustThisUrl', ["$sce", function ($sce) {
      return function (val) {
        return $sce.trustAsResourceUrl(val);
      };
    }]);

    function pluck(objects, property) {
      if (!(objects && property && angular.isArray(objects))) return [];

      property = String(property);
      
      return objects.map(function(object) {
        // just in case
        object = Object(object);
        
        if (object.hasOwnProperty(property)) {
          return object[property];
        }
        
        return '';
      });
    }
})();
(function() {
    'use strict';

    angular
        .module('app.core')
        .run(appRun);

    /*  */
    function appRun(routerHelper) {
        var otherwise = '/404';
        routerHelper.configureStates(getStates(), otherwise);

        function getStates() {
            return [
                {
                    state: '404',
                    config: {
                        url: '/404',
                        templateUrl: 'app/core/404.html',
                        title: '404'
                    }
                }
            ];
        }
    }
})();

(function () {
    'use strict';

    angular
        .module('app.core')
        .factory('dataservice', dataservice);

    dataservice.$inject = ['$http', '$q', 'exception', 'logger', 'myservice', 'MainService', 'uploadApi', '$translate', '$location'];
    /*  */
    function dataservice($http, $q, exception, logger, myservice, MainService, uploadApi, $translate, $location) {
        var service = {
            getStations: getStations,
            getStation: getStation,
            getStationChildren: getStationChildren,
            affiliateStation: affiliateStation,
            unaffiliateStation: unaffiliateStation,
            station: {},
            saveStation: saveStation,
            toggleStationStatus: toggleStationStatus,
            setCreativeFields: setCreativeFields,
            getNbCodes: getNbCodes,
            getCodes: getCodes,
            getCodesSerials: getCodesSerials,
            generateCodes: generateCodes,
            generateCodesSerial: generateCodesSerial,
            saveClient: saveClientParams,
            getClientProviders: getClientProviders,
            getSsoSetting: getSsoSetting,
            saveSsoSetting: saveSsoSetting
        };

        return service;




        function getStations(filter, config) {
            if (!!filter) {
                config.params = config.params || {};
                config.params = angular.extend(config.params, {name: filter});
            }
            return $http.get(MainService.stationsEndpoint(), config)
                .then(success)
                .catch(fail);

            function success(response) {
                return response.data;
            }

            function fail(e) {
                return exception.catcher('XHR Failed for getStations')(e);
            }
        }

        function getStation(id) {
            return $http.get(MainService.stationsEndpoint() +'/' + id)
                .then(success)
                .catch(fail);

            function success(response) {
                return response.data;
            }

            function fail(e) {
                return exception.catcher('XHR Failed for getStation ' + id)(e);
            }
        }

        function getStationChildren(id) {
            return $http.get(MainService.stationsEndpoint()+'/' + id + '/children')
                .then(success)
                .catch(fail);

            function success(response) {
                return response.data;
            }

            function fail(e) {
                return exception.catcher('XHR Failed for getStationChildren ' + id)(e);
            }
        }

        function affiliateStation(id, parent_id) {
            return $http.get(MainService.stationsEndpoint()+'/' + id + '/affiliate_with/' + parent_id)
                .then(success)
                .catch(fail);

            function success(response) {
                return response.data;
            }

            function fail(e) {
                return exception.catcher('XHR Failed for getStationChildren ' + id)(e);
            }
        }

        function unaffiliateStation(id, parent_id) {
            return $http.get(MainService.stationsEndpoint()+'/' + id + '/unaffiliate')
                .then(success)
                .catch(fail);

            function success(response) {
                return response.data;
            }

            function fail(e) {
                return exception.catcher('XHR Failed for getStationChildren ' + id)(e);
            }
        }

        function saveStation(station) {
            var deferred = $q.defer();

            var endpoint = 'stations' + (station.id ? '/'+station.id : '');
            var params = { station: {...station}, endpoint: endpoint };
            if (station.validity_due_date) params.station.validity_due_date = station.validity_due_date.toISOString();
            var config = {};
            config.method = station.id ? 'PUT' : 'POST';
            uploadApi.upload(params, config)
            .then(function(response) {
                deferred.resolve(response);
            }, function(e) {
              $translate("ITEM_ERROR").then(function(toast) {
                logger.error(toast);
              });
            });

            return deferred.promise;
        }

        function setCreativeFields(station, creativeFields) {
            var deferred = $q.defer();

            var endpoint = MainService.stationsEndpoint()+ '/' + station.id + '/apps';
            var params = { creative_fields: creativeFields, endpoint: endpoint };
            return $http.put(endpoint, params)
                .then(success)
                .catch(fail);

            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for saveClient ' + station.id)(e);
            }
        }

        function getNbCodes(station, params) {
            return $http.get(MainService.stationsEndpoint()+'/' + station.id + '/codes', {params: $.extend(params, {count: '✓'})})
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for getNbCodes ' + station.id)(e);
            }
        }

        function getCodes(station, params) {
            console.log(params)
            return $http.get(MainService.stationsEndpoint()+'/' + station.id + '/codes', {params: params})
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for getCodes ' + station.id)(e);
            }
        }

        function getCodesSerials(station, params) {
            return $http.get(MainService.stationsEndpoint()+'/' + station.id + '/codes/serials', {params: params})
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for getCodesSerials ' + station.id)(e);
            }
        }

        function generateCodes(nbCodes, station, features) {
            var args = {nb_codes: nbCodes, coupon_invitation_code: {features: features}}
            return $http.post(MainService.stationsEndpoint()+'/' + station.id + '/codes', args)
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for generateCodes ' + station.id)(e);
            }
        }

        function generateCodesSerial(nbCodes, station, features, serial_name) {
            var args = {
                nb: nbCodes, 
                coupon_invitation_code: {features: features, station_id: station.id},
                serial: {name: serial_name}
            }
            return $http.post(MainService.stationsEndpoint()+'/' + station.id + '/codes/serial', args)
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for generateCodesSerial ' + station.id)(e);
            }
        }

        function getClientProviders(params) {
            return $http.get(MainService.stationsEndpoint()+'/providers', {params: params})
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for getClientProviders')(e);
            }
        }

        function saveClientParams(client, station) {
            var fct = client.id ? $http.put : $http.post;
            var clientId = client.id ? '/' + client.id : '';
            return fct(MainService.stationsEndpoint()+'/' + station.id + '/clients' + clientId, {client: client})
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for saveClient ' + station.id)(e);
            }
        }

        function getSsoSetting(station, params) {
            return $http.get(MainService.stationsEndpoint()+'/' + station.id + '/sso_setting', {params: params})
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for getSsoSetting')(e);
            }
        }

        function saveSsoSetting(setting, station) {
            var fct = setting.id ? $http.put : $http.post;
            var settingId = setting.id ? '/' + setting.id : '';

            return fct(MainService.stationsEndpoint()+'/' + station.id + '/sso_settings' + settingId, {sso_setting: setting})
                .then(success)
                .catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for saveSsoSetting for station ' + station.id)(e);
            }
        }

        function toggleStationStatus(station, active) {
            var action = active ? 'activate' : 'stop';
            return $http.post(MainService.stationsEndpoint()+'/' + station.id + '/' + action).then(success).catch(fail);
            function success(response) {
                return response.data;
            }
            function fail(e) {
                return exception.catcher('XHR Failed for ' + action + ' station ' + station.id)(e);
            }
        }
    }
})();

angular.module('app.core').service('ItemsApi', ['$resource', 'MainService', function($resource, MainService) {
  return $resource(
    MainService.commonApiEndpoint() + ':endpoint/:key/:resource/:resource_key/:action',
    { endpoint: '@endpoint', key: '@id', action: '@action' },
    {
        getLatest:          {method: 'GET', params: {key: 'latest'}, isArray: false},
        filter:             {method: 'GET', isArray: false},
        update:             {method: 'PUT'},
        similars:           {method: 'GET', params: {action: 'similars'}, isArray: true}
    });
  }
]);

(function() {
      "use strict";

      angular.module('app.core').config(['$translateProvider', function($translateProvider) {

            $translateProvider.useSanitizeValueStrategy(null);
            $translateProvider.translations('fr',
              {
                  "ACCESS_RESOURCE": "Accéder",
                  "ACCESS_DENIED": "Accès refusé",
                  "ACTION_NOW": "Voir",
                  "ACTIVE": "Actif",
                  "ADD": "Ajouter",
                  "ADD: {{ item_type | translate }}": "Ajouter : {{ item_type | translate }}",
                  "ADD_DESC": "Ajouter une description",
                  "ADD_TO_PLAYLIST": "Ajouter à une playlist",
                  "ADD_TO_STOCK": "Ajouter à une capsule",
                  "ADDRESS": "Adresse",
                  "ALL": "Tout",
                  "ASK_FOR_PROMOTION": "Suggérer pour une mise en avant",
                  "ASSISTANTS": "Assistants",
                  "BACK": "Retour",
                  "BACK_TO_SIGN_UP_FORM": "Retour au formulaire de création de compte",
                  "BOOK": "Livre",
                  "BROWSE": "Parcourir",
                  "BROWSE_WITH_GENRE": "Parcourir : {{ genre }}",
                  "CANCEL_ASK_FOR_PROMOTION": "Annuler la suggestion de mise en avant",
                  "CAPSULES": "Capsules",
                  "CHANGE_LEVEL": "Changer de statut",
                  "CHANGE_LEVEL_USER": "Passer en manager",
                  "CHANGE_LEVEL_MANAGER": "Passer en utilisateur",
                  "STATIONS.CHILDREN": "Stations filles",
                  "CHOOSE_NEW_LEVEL": "Choisir le nouveau statut",
                  "CHOOSE_NEW_STATION": "Choisir la nouvelle station",
                  "CITY": "Ville",
                  "CONFIRM": "Confirmer",
                  "CONFIRM_DELETE_QUESTION": "Êtes-vous sûr de vouloir supprimer cet élément ?",
                  "CONTACT_US": "Contactez-nous",
                  "CONTINUE": "Continuer",
                  "COUNTRY": "Pays",
                  "COVER": "Image principale",
                  "COVER_PLACEHOLDER": "Image principale...",
                  "CREATED": "Créé",
                  "CREATED_AT": "Créé",
                  "UPDATED_AT": "Modifié",
                  "RENEWED_AT": "Renouvelé",
                  "LAST_SIGN_IN": "Dernière connexion",
                  "CREATION_YEAR": "Année de création",
                  "CREATION_YEAR_PLACEHOLDER": "Année de création...",
                  "CREATIVE_FIELDS": "Champs créatifs",
                  "CREATIVE_FIELDS_INVITATION": "Invitation au(x) champ(s) créatif(s)",
                  "CREATIVE_FIELDS_MUNKI_CREATION": "Créer un compte Munki",
                  "CREATIVE_FIELDS.HINT": "L'usager recevra une invitation par email.",
                  "CREATIVE_FIELDS_MUNKI.HINT": "L'usager recevra par email ses codes de connexion à l'application et le lien de téléchargement.",
                  "CREATIVE_FIELD_MISSING": "Ce champ créatif n'est pas activé pour la station de cet utilisateur. N'hésitez pas à prendre contact avec notre équipe commerciale via la page Services pour demander son activation.",
                  "CREATIVE_FIELD.UNACTIVABLE": "Ce champ créatif n'est pas encore disponible mais le sera prochainement. N'hésitez pas à prendre contact avec notre équipe commerciale via la page Services.",
                  "CSV_FILE": "Fichier CSV",
                  "DASHBOARD": "Dashboard",
                  "DATA_PROCESSED": "Traitement terminé :)",
                  "DELETED": "Bloqué",
                  "DESCRIPTION": "Description : {{ lang }}",
                  "DESCRIPTION": "Description",
                  "DESCRIPTION_PLACEHOLDER": "Description...",
                  "DEVELOPERS": "Développeurs",
                  "DEVELOPERS_PLACEHOLDER": "Développeurs...",
                  "DISABLED": "Désactivé",
                  "DISCONNECTED_MSG": "Vous êtes maintenant déconnecté.\n" +
                                      "Merci de votre visite.",
                  "DISCOVER": "Découvrir",
                  "DISCOVER_NEW_AUTHORS": "Et si vous découvriez de nouveaux artistes&nbsp;?",
                  "DISTRIBUTOR": "Distributeur",
                  "DISTRIBUTOR_PLACEHOLDER": "Distributeur...",
                  "DIVERCITIES": "Divercities",
                  "DOWNLOAD": "Télécharger",
                  "DOWNLOADABLE_GAMES": "Jeux téléchargeables",
                  "DOWNLOADS_LIMIT_REACHED": "Limite de téléchargements atteinte",
                  "DROP_FILE_HERE": "Déposer un fichier ici",
                  "DROP_FILES_HERE": "Déposer des fichiers ici",
                  "EDIT": "Éditer",
                  "EDIT_GAME": "Éditer : <em>{{ item }}</em>",
                  "EDIT_WORK": "Éditer : <em>{{ work }}</em>",
                  "EDITORS": "Editeurs",
                  "EMAIL_ADDRESS": "Adresse email",
                  "EMAIL.INVALID": "L'email indiqué est invalide",
                  "EMAIL_ADDRESS_PLACEHOLDER": "Adresse email...",
                  "ENABLED": "Activé",
                  "EVERYONE": "Tous",
                  "EXPIRED": "Expiré",
                  "FAIR_STREAMING": "Streaming équitable",
                  "FILTER": "Filtre",
                  "FILTER_PLACEHOLDER": "Filtrer...",
                  "FIRST_NAME": "Prénom",
                  "FOR_STATION": "pour {{ station_name }}",
                  "FORGOTTEN_PASSWORD": "Mot de passe oublé ?",
                  "FORK": "Importer",
                  "FORKED_N_TIMES": "Importée {{ n }} fois",
                  "FORM.BAD_VALUES": "Certains champs sont incorrects",
                  "FROZEN": "Gelé",
                  "GAME": "Jeu vidéo",
                  "GAME_APP": "Divercities Game",
                  "GAME_PITCH": "L'histoire",
                  "GAME_REQUIREMENTS": "Configuration requise",
                  "GAMES_STUDIO_MADE": "Jeux",
                  "GENRES": "Genres",
                  "GENRES": "Genres",
                  "GENRES_PLACEHOLDER": "Genres...",
                  "GOTO_DETAILS": "Voir la fiche détaillée",
                  "HELP": "Aide",
                  "IMAGES": "Images",
                  "IMAGES_PLACEHOLDER": "Déposer des images...",
                  "IN_STOREROOM": "Dans la Réserve",
                  "INDIE_CREATION": "La création indépendante",
                  "INDIE_CREATIVES": "Les créateurs indépendants",
                  "INVITED": "L'utilisateur n'a pas finalisé son inscription",
                  "INVITED_AT": "Invité",
                  "ITEM_CREATED": "{{ item_type | translate }} créé",
                  "ITEM_ERROR": "Erreur lors du traitement",
                  "KEY": "Clé",
                  "KEYSTEAM": "Clé Steam",
                  "LAST_CONNECTED_AT": "Dernière connexion",
                  "LAST_NAME": "Nom de famille",
                  "LATEST_GAMES_STUDIO_MADE": "Dernières sorties",
                  "LETS_SIGNUP": "Bienvenue !\n" +
                                 "Créons votre compte Divercities",
                  "LEVEL": "Statut",
                  "LEVEL_PLACEHOLDER": "Statut...",
                  "LIMIT_REACHED": "Limite atteinte",
                  "LOGIN": "Login",
                  "LOGIN_PLACEHOLDER": "Login",
                  "LOGO": "Logo",
                  "LOGO_PLACEHOLDER": "Logo...",
                  "LOGOUT": "Se déconnecter",
                  "MANAGERS": "Managers",
                  "MEMBERS": "membres",
                  "MONTHS": "Mois",
                  "MOVE_USER": "Déplacer l'utilisateur",
                  "MRA_NUMBER": "Votre numéro de carte M'ra",
                  "MRA_PASSWORD": "Votre Mot de passe M'ra",
                  "MRA_PASSWORD_FORGOTTEN": "Mot de passe M'ra oublié ?",
                  "MRA_PASSWORD_HINT": "Par défaut, votre date de naissance au format \"JJ/MM/AAAA\"",
                  "MUNKI_PROVIDER_MISSING": 'Un provider id munki doit être fourni pour activer le champ créatif MUNKI',
                  "MUSIC": "Musique",
                  "MUSIC_APP": "Divercities Music",
                  "MUNKI": "Munki",
                  "MY_ACCOUNT": "Mon compte",
                  "MY_CAPSULES": "Mes capsules",
                  "MY_GAME_LIBRARY": "Ma librairie",
                  "MY_GAMES": "Mes jeux vidéo",
                  "MY_LIBRARY": "Vide poche",
                  "MY_MUSICS": "Ma musique",
                  "MY_PLACE": "Mon lieu",
                  "N_FORKS": "{{ n }} forks",
                  "NAME": "Nom",
                  "NAME_PLACEHOLDER": "Nom...",
                  "NB_ITEMS": "{{ nb }} éléments",
                  "NEW_PLAYLIST": "Nouvelle playlist",
                  "NEW_STOCK": "Nouvelle capsule",
                  "NEW_THINGS": "Nouveautés",
                  "NO_CAPSULE": "Aucune capsule pour l'instant...",
                  "NO_RESULTS": "Aucun résultat",
                  "NONE": "Aucun",
                  "OAUTH_CLIENT_ID": "Client ID",
                  "OAUTH_CLIENT_SECRET": "Client secret",
                  "OAUTH_CLIENT_OPTIONS": "Client options",
                  "OAUTH_SCOPE": "Scope",
                  "OFFERED": "Jeu offert",
                  "PARAMETERS": "Paramètres",
                  "PARENT_ID": "ID Station Mère",
                  "PASSWORD": "Mot de passe",
                  "PHONE.INVALID": "Le numéro de téléphone est invalide",
                  "PLACES_1D_TOUCH": "Lieux Divercities",
                  "PROCESSING_ERROR": "Erreur lors du traitement de votre requête",
                  "PRODUCER": "Producteur",
                  "PRODUCER_PLACEHOLDER": "Producteur...",
                  "PROMOTE": "Afficher dans «Découvrir»",
                  "PROMOTED": "Mise en avant",
                  "RATING": "Notations",
                  "RATING_HINT_BAD": "Mauvais",
                  "RATING_HINT_GOOD": "Bon",
                  "RATING_HINT_GORGEOUS": "Énorme",
                  "RATING_HINT_POOR": "Pas terrible",
                  "RATING_HINT_REGULAR": "Moyen",
                  "RE_INVITE": "Ré-inviter",
                  "REDACTOR_HINT": "Sélectionner du texte pour le mettre en forme",
                  "RELEASE_YEAR": "Année de sortie",
                  "RELEASE_YEAR_PLACEHOLDER": "Année de sortie...",
                  "RETRIEVE_MRA_PASSWORD_HINT": "Pour récupérer votre mot de passe M'ra, veuillez compléter le formulaire ci-dessous",
                  "SAVE": "Enregistrer",
                  "SAVE_MODIFICATIONS": "Enregistrer les modifications",
                  "SCREENSHOTS": "Captures d'écran",
                  "SCREENSHOTS_PLACEHOLDER": "Déposer des captures d'écran...",
                  "SEARCH": "Rechercher",
                  "SEARCH_PLACEHOLDER": "Rechercher...",
                  "SEND": "Envoyer",
                  "SERVICES": "Le Kiosque Divercities",
                  "SERVICES.SUBTITLE": "Vos différents services réunis dans le Kiosque",
                  "SERVICES.CONTACT_REQUEST_MORE": "Plus d'informations sur ",
                  "SERVICES.CONTACT_REQUEST_TEXT": "Besoin d'aide ? Contactez nos équipes !\n" + 
                                                   "Indiquez le numéro de téléphone et l'adresse mail auxquels vous souhaitez être contacté et nous reviendrons vers vous rapidement.",
                  "SERVICES.STATS_REQUEST_TEXT": "Demandez votre rapport de statistiques.",
                  "SERVICES.CONTACT_REQUEST_SUBMIT": "Être recontacté",
                  "SERVICES.NOT_ACTIVATED": "Vous n'êtes pas encore abonné à ce service",
                  "SERVICES.FIELD_REQUIRED": "L'un des deux champs doit être rempli",
                  "SERVICES.DOCUMENTATION.TITLE": "Documentation",
                  "SERVICES.DOCUMENTATION.LINK": "Découvrez l’ensemble des ressources du kiosque Divercities",
                  "SERVICES.RGPD.TITLE": "Conformité RGPD",
                  "SERVICES.RGPD.LINK": "Accéder à la page dédiée",
                  "SIGN_IN": "Se connecter",
                  "SIGN_OUT": "Se déconnecter",
                  "SIGN_UP": "Inscription",
                  "SIGNUP_UNAVAILABLE": "Inscription temporairement indisponible",
                  "SIMILAR_GAMES": "Jeux similaires",
                  "STATION": "Lieu",
                  "STATION_ID": "Station ID",
                  "STATION_MEMBERS": "{{ count }} membres",
                  "STATIONS": "Stations",
                  "STATIONS.SSO_LINK": "Lien SSO",
                  "STATIONS.STOP": "Arrêter la station",
                  "STATIONS.STATS": "Statistiques",
                  "STATIONS.STATS.FACTOR": "Facteur",
                  "STATIONS.SUBTITLE": "Toutes les stations Divercities",
                  "STATIONS.UI": "UI",
                  "STATIONS.UI.HINT": "Version de l'interface diMusic",
                  "STATIONS.CHECK_PORTAL_ACCOUNT_VALIDITY.HINT": "Vérifier la validité du compte portail : empêche le téléchargement si le compte n'est pas validé coté portail.",
                  "STATIONS.SEARCH_ALL": "Rechercher dans les stations filles",
                  "STATIONS.ACTION": "Action",
                  "STATIONS.ACCESS": "Accès",
                  "STATIONS.ACTIVE_ACCOUNTS": "Actifs",
                  "STATIONS.INACTIVE_ACCOUNTS": "Inactifs",
                  "STATIONS.ADD_PLACE": "Ajouter un lieu",
                  "STATIONS.ADD_STATION": "Ajouter un lieu",
                  "STATIONS.AFFILIATE_TO": "Affilier à",
                  "STATIONS.ASSOCIATE_CLIENT": "Associer un client",
                  "STATIONS.AUTHORIZE_ACCESS": "Autorise l'accès",
                  "STATIONS.AUTHORIZE_HANDLING": "Autorise la gestion par les managers",
                  "STATIONS.CITY": "Ville",
                  "STATIONS.CLIENT": "Paramètres Client",
                  "STATIONS.CLIENT.MUNKI_PROVIDER_ID": "ID du provider associé chez Munki",
                  "STATIONS.CLIENT.TENK_PROVIDER_ID": "ID du groupe associé chez Tënk",
                  "STATIONS.CLIENT.NAME": "Nom du client",
                  "STATIONS.CODES": "Codes d'activation",
                  "STATIONS.CODES_ALL": "Tous les codes",
                  "STATIONS.CODES_DESCRIPTION": "Les codes d'activation sont des codes à usage unique qui permettent à un utilisateur de créer son compte sur la plateforme Divercities.\n" +
                                                "Une fois le code généré, il peut être utilisé sur la page <a ng-href=\"{{use_url}}\">{{use_url}}</a>. Si le code est validé, l’utilisateur sera invité à choisir son mot de passe et à se connecter sur votre station Divercities. Un code ne peut être utilisé qu’une seule fois, une fois validé, il devient alors obsolète et ne permet plus l’ouverture de compte.",
                  "STATIONS.CODES_FEATURES": "Features du code",
                  "STATIONS.CODES_USED": "État du code",
                  "STATIONS.CODES_QUOTA": "Codes générés / Nombre maximal générable",
                  "STATIONS.CODES_SERIAL": "Série",
                  "STATIONS.CODES_SERIALS": "Séries de codes",
                  "STATIONS.CODES_SERIAL_NAME": "Nom de la série",
                  "STATIONS.CODES_SERIAL_NUMBER": "Numéro de série",
                  "STATIONS.CODES_SERIAL_COUNT": "Nombre de codes",
                  "STATIONS.CODES_SERIAL_FEATURES": "Features des codes",
                  "STATIONS.CODES_VALUE": "Valeur du code",
                  "STATIONS.CODES.FILTER": "Filtre",
                  "STATIONS.COMMON.SETTINGS.AUTHORIZE_STATION_TO_GENERATE_CODES": "Autorise la génération de codes d'activation par les managers",
                  "STATIONS.COMMON.SETTINGS.AUTHORIZE_STATION_TO_USE_CODES": "Autorise l'utilisation des codes d'activation par les utilisateurs",
                  "STATIONS.COMMON.SETTINGS.CODES_LIMIT": "Nombre maximal de codes d'activation générables par la station",
                  "STATIONS.COMMON.SETTINGS.CODES_LIMIT.HINT": "Laisser vide pour laisser la station générer un nombre illimité de codes d'activation",
                  "STATIONS.COMMON.SETTINGS.MANAGERS_CAN_TO_SOFT_DELETE": "Autoriser les managers à supprimer des utilisateurs",
                  "STATIONS.COMMON.SETTINGS.MANAGERS_CAN_TO_SOFT_DELETE.HINT": "Si coché, permet aux managers de supprimer les utilisateurs de leur station",
                  "STATIONS.COMMON.SETTINGS.SEND_MANAGERS_NOTIFICATIONS": "Envoyer des notifications aux managers",
                  "STATIONS.COMMON.SETTINGS.SEND_MANAGERS_NOTIFICATIONS.HINT": "Si coché, un mail sera envoyé à tous les managers de la station à chaque création SSO d'un compte utilisateur sur la station.",
                  "STATIONS.COMMON.SETTINGS": "Paramètres communs",
                  "STATIONS.CONTRACT_DATE": "Date de début de contrat",
                  "STATIONS.COUNTRY": "Pays",
                  "STATIONS.DEFAULT_CREATIVE_FIELDS": "Champs créatifs par défaut",
                  "STATIONS.DEFAULT_CREATIVE_FIELDS.HINT": "Les champs créatifs automatiquement autorisés pour les nouveaux usagers",
                  "STATIONS.DESCRIPTION": "Gérez votre communauté, consultez vos statistiques.",
                  "STATIONS.FORCE_ALL_CREATIVE_FIELDS_ACCESS": "Autorise l'accès à un champ créatif à la volée",
                  "STATIONS.FORCE_ALL_CREATIVE_FIELDS_ACCESS.HINT": "Autorise un utilisateur à accéder à un champ créatif même s'il n'est pas coché dans son compte",
                  "STATIONS.FROZEN_ACCOUNTS": "Désactivés",
                  "STATIONS.GAME.SETTINGS": "Paramètres Jeux vidéos",
                  "STATIONS.GENERATE_CODES.CREATIVE_FIELDS": "Champ créatifs",
                  "STATIONS.GENERATE_CODES.CREATIVE_FIELDS.HINT": "Les champs créatifs autorisés pour l'utilisateur du code",
                  "STATIONS.GENERATE_CODES.FEATURES": "Features",
                  "STATIONS.GENERATE_CODES.LABEL": "Nombre de codes à générer",
                  "STATIONS.GENERATE_CODES.MAX_DOWNLOADS": "Nombre de téléchargements (jeux vidéos)",
                  "STATIONS.GENERATE_CODES.MAX_DOWNLOADS.HINT": "le nombre maximal de téléchargements autorisé pour l'utilisateur du code sur la plateforme jeux vidéos",
                  "STATIONS.GENERATE_CODES.VALIDITY": "Durée de validité du compte (en jours)",
                  "STATIONS.GENERATE_CODES.VALIDITY.HINT": "La durée de validité de l'utilisateur du code (en jours)",
                  "STATIONS.GENERATE_CODES.INCLUDE_SERIAL": "Inclure les codes dans une série",
                  "STATIONS.GENERATE_CODES.SERIAL_NAME": "Nom de la série",
                  "STATIONS.GENERATE_CODES": "Générer des codes",
                  "STATIONS.GENERATED_CODES": "Nombre total de codes générés",
                  "STATIONS.HAS_VALIDITY_DUE_DATE": "Active l'utilisation d'une date de fin de validité pour les utilisateurs de la station",
                  "STATIONS.ID": "ID",
                  "STATIONS.STATUS": "État",
                  "STATIONS.INFORMATIONS": "Informations",
                  "STATIONS.INVITE_SOMEONE": "Inviter quelqu'un",
                  "STATIONS.INVITED_ACCOUNTS": "Invités",
                  "STATIONS.LOGO_INFO": "Logo actuel",
                  "STATIONS.MANAGER_HANDLE_VALIDITY": "Gestion de la validité des comptes par les managers",
                  "STATIONS.MANAGER_HANDLE_VALIDITY.HINT": "Lorsque cette case est cochée, les managers de la station peuvent gérer eux-même la validité des comptes.",
                  "STATIONS.MASS_INVITE": "Inviter plusieurs personnes",
                  "STATIONS.MAX_STATION_DOWNLOADS": "Nombre maximal de jeux téléchargeables par station",
                  "STATIONS.MAX_USER_DOWNLOADS": "Nombre maximal de jeux téléchargeables par usager",
                  "STATIONS.MAX_USER_DOWNLOADS.HINT": "Nombre maximal de jeux différents que l'usager peut télécharger.\n" +
                                                      "(limite contractuelle)",
                  "STATIONS.MUSIC.SETTINGS": "Paramètres Musique",
                  "STATIONS.NAME": "Nom",
                  "STATIONS.PERIOD": "Période",
                  "STATIONS.REMAINING_CODES": "Codes non consommés",
                  "STATIONS.SETTINGS": "Paramètres {{ creative_field | capitalize }}",
                  "STATIONS.SUBSCRIPTION": "Abonnement",
                  "STATIONS.SSO_SETTING": "SSO",
                  "STATIONS.SSO_SETTING.BIBID": "BibID",
                  "STATIONS.SSO_SETTING.CHECK_USER_VALIDITY_AT_SIGNIN": "Vérifie la validité du compte utilisateur au signin",
                  "STATIONS.SSO_SETTING.PROVIDER": "CMS",
                  "STATIONS.SSO_SETTING.USER_CAN_CHOOSE_PASSWORD": "Affiche le formulaire de saisie de mot de passe",
                  "STATIONS.SSO_SETTING.PORTAL_URL": "URL portail",
                  "STATIONS.UNLIMITED": "Illimité",
                  "STATIONS.USED_CODES": "Codes consommés",
                  "STATIONS.UNAFFILIATE": "Désaffilier",
                  "STATIONS.VALIDITY_DUE_DATE": "Date limite",
                  "STATIONS.VALIDITY_DUE_DATE.HINT": "Cette date définit la validité de TOUS les comptes de la station. \n\n Laissez cette date vide si vous préférez gérer la validité des comptes indépendemment pour chaque utilisateur lors de la création des comptes.",
                  "STATIONS.VALIDITY_DURATION": "Durée de validité des comptes",
                  "STATIONS.WELCOME_TEXT": "Texte de bienvenue",
                  "STATIONS.WELCOME_TEXT.HINT": "Ce texte sera envoyé par email aux utilisateurs de la station après la création de leur compte. Laisser vide pour un texte standard." +
                                                      "Vous pouvez également utiliser les variables suivantes : <br> - [[user]] (nom d'utilisateur) et <br> - [[email]] (email de l'utilisateur) <br> - [[pack]] (nom du pack, si les utilisateurs sont susceptibles d'utiliser des codes d'invitation appartenant à un pack)",
                  "STATIONS.INVITATION_TEXT": "Texte d'invitation",
                  "STATIONS.INVITATION_TEXT.HINT": "Ce texte sera envoyé par email lors de l'invitation d'une utilisateur par un manager de la station. Laisser vide pour un texte standard.<br><br>" +
                                                      "Vous pouvez également utiliser les variables suivantes : [[user]] (nom d'utilisateur) et [[email]] (email de l'utilisateur)",
                  "STATIONS_INVITATION_MESSAGE": "Bonjour [[user]] !\n\n" + 
                                                 "Vous avez reçu une invitation de {{ station.name }} pour rejoindre Divercities, la première plateforme de streaming entièrement centrée sur la&nbsp;création indépendante&nbsp;!\n" +
                                                 "\n" +
                                                 "Divercities ('indé' touch) repose sur un modèle économique innovant et plus juste pour les artistes et les producteurs, notamment à travers un nouveau système de partage de la valeur : la&nbsp;contribution créative territoriale.&nbsp;\n" +
                                                 "\n" +
                                                 "Vous aussi, osez le&nbsp;streaming équitable&nbsp;et découvrez plus d'1 million de titres&nbsp;indépendants !",
                  "STATIONS_INVITATION_MESSAGE_LABEL": "Le message sera suivi d'un bouton permettant à l'utilisateur d'accepter son invitation",
                  "STATS": "Les statistiques",
                  "STATS.SUBTITLE": "Les statistiques de vos services",
                  "STATS.ALL_STATIONS": "Toutes les stations",
                  "STATS.COMPARISON_PERIOD": "Période de comparaison",
                  "STATS.DOWNLOAD.HINT": '<div class="title">Comment ouvrir l\'export CSV</div><div class="description"><dt>LibreOffice/GoogleSheet</dt><dd>Sélectionner l\'encodage UTF8<br/>Sélectionner le séparateur <code>;</code> (point-virgule)</dd><dt>Excel</dt><dd>La détection est automatique</dd></div>',
                  "STATS.EXPORT": "Exporter",
                  "STATS.EXPORTING": "Export en cours",
                  "STATS.GA3": "Jusqu'au 1er Juillet 2023",
                  "STATS.GA4": "À partir du 1er Juillet 2023",
                  "STATS.LOADING": "En cours de chargement",
                  "STATS.NB_RESULTS": "{{nb}} résultats",
                  "STATS.NEW_SEGMENT": "Nouveau segment",
                  "STATS.NO_SEGMENT": "Aucun segment",
                  "STATS.NO_ACCESS": "Vous n'avez pas accès aux statistiques car le service choisi n'est pas activé pour votre station.<br><br>" +
                                     "N'hésitez pas à prendre contact avec notre équipe commercial via la page Services pour demander l'activation de ce service",
                  "STATS.ACCESS_SERVICES": "Accéder à la page services",
                  "STATS.NODATA": "Aucune donnée disponible",
                  "STATS.PERIOD": "Période",
                  "STATS.REMOVE_ALL": "Tout supprimer",
                  "STATS.SAMPLE": "Echantillon",
                  "STATS.SEGMENT": "Segment",
                  "STATS.STATION": "Station",
                  "STATS.SERVICE": "Service",
                  "STATS.NO_SERVICE": "Pas de services",
                  "STATS.ALL_SERVICE": "Tous les service",
                  "STATS.USER_SAMPLE": "Échantillon d'utilisateurs",
                  "STEAM_PLACEHOLDER": "Clé Steam...",
                  "STOCK": "La réserve",
                  "STOREROOM": "La réserve 1D",
                  "STUDIO": "Studio",
                  "STUDIO_PLACEHOLDER": "Studio...",
                  "STUDIOS": "Studios",
                  "STUDIOS_PLACEHOLDER": "Studios...",
                  "SUBSCRIBE_CAPSULES": "Capsules abonnés",
                  "TENK": "Tënk",
                  "TENK_PROVIDER_MISSING": 'Un group id tenk doit être fourni pour activer le champ créatif Tënk',
                  "TITLE": "Titre",
                  "TITLE_PLACEHOLDER": "Titre...",
                  "TOS": "CGU",
                  "TOURS_ADMIN": "Administration des tours",
                  "TV_APP": "Divercities TV",
                  "UNAUTHORIZED_MSG": "Vous n'êtes pas autorisé à vous connecter à {{ app }}.\n" +
                                      "Rapprochez-vous du manager de votre lieu.",
                  "UNPROMOTE": "Retirer de «Découvrir»",
                  "UPDATE": "Mettre à jour",
                  "USER": "Utilisateur",
                  "USER.INFORMATIONS": "Informations",
                  "USER.CREATIVE_FIELDS": "Accès aux champs créatifs",
                  "USER.CREATIVE_FIELD_VALIDITY_DUE_DATE": "Date de validité de l'accès",
                  "USER.CREATIVE_FIELD_MAX_USER_DOWNLOADS": "Nombre de téléchargements max",
                  "USER.OFFERS": "Offres utilisées",
                  "USER.OFFERS_ALL": "Toutes les offres utilisés",
                  "USERS": "Utilisateurs",
                  "USERS.SUBTITLE": "Tous les utilisateurs et leurs services",
                  "USERS.ACTION": "Action",
                  "USERS.ACTIVE_ACCOUNTS": "Comptes actifs",
                  "USERS.INACTIVE_ACCOUNTS": "Comptes inactifs",
                  "USERS.CLEAN": "Nettoyer",
                  "USERS.CONTRIB": "Contribution créative",
                  "USERS.DELETE": "Supprimer",
                  "USERS.DELETED": "Supprimé",
                  "USERS.DESCRIPTION": "Gérez votre communauté, consultez vos statistiques.",
                  "USERS.EXPIRED": "Expiré",
                  "USERS.EXPORT": "Exporter",
                  "USERS.EXPORT.TOOLTIP": "Exporter la vue actuelle",
                  "USERS.FREEZE": "Geler",
                  "USERS.FREEZE_FROM": "Geler les comptes inactifs depuis",
                  "USERS.FROZEN": "Le compte de l'utilisateur est gelé",
                  "USERS.FROZEN_ACCOUNTS": "Comptes gelés",
                  "USERS.ID": "ID",
                  "USERS.INVITATIONS_SENT": "Invitations envoyées",
                  "USERS.CREATE": "Créer un compte",
                  "USERS.CREATE.MULTIPLE": "Créer plusieurs comptes",
                  "USERS.INVITE": "Invitation solo",
                  "USERS.INVITE.MULTIPLE": "Invitations groupées",
                  "USERS.ACTIVITY": "Activité",
                  "USERS.LAST_ACTIVITY": "Dernière activité",
                  "USERS.LAST_LISTENING": "Dernière écoute",
                  "USERS.LAST_SIGN_IN": "Dernière connexion",
                  "USERS.LEVEL": "Statut",
                  "USERS.LISTENING": "Écoutes",
                  "USERS.MONTHS": "mois",
                  "USERS.NAME": "Nom",
                  "USERS.NEVER": "Jamais",
                  "USERS.REINVITE": "Ré-inviter",
                  "USERS.REINVITE.TOOLTIP": "Relancer toutes les invitations",
                  "USERS.REINVITE_CONFIRM": "Ré-inviter tous les comptes en attente, pour la station <em>{{station.name}}</em> ?",
                  "USERS.REINVITE_UNAVAILABLE": "Impossible de réinviter tous les comptes sans indiquer une station",
                  "USERS.REMOTE_ACCOUNTS": "Comptes distants",
                  "USERS.REMOTE_FREEZE": "Geler le compte {{ service }} distant",
                  "USERS.REMOTE_UNFREEZE": "Dégeler le compte {{ service }} distant",
                  "USERS.REMOTE_REINVITE": "Renvoyer les instructions de connexion à {{ service }}",
                  "USERS.RENEW": "Renouveler le compte",
                  "USERS.SHOW": "Voir les détails",
                  "USERS.STATION": "Station",
                  "USERS.STATUS": "État",
                  "USERS.TRANSFER": "Transférer",
                  "USERS.UI": "UI",
                  "USERS.UI.HINT": "Version de l'interface diMusic",
                  "USERS.UNFREEZE": "Dégeler",
                  "USERS.UPDATE": "Mettre à jour les nom/email",
                  "USERS.UPDATE_VALIDITY": "Enregistrer la date de fin de validité",
                  "USERS.UPDATE_SETTINGS": "Enregistrer les préférences",
                  "USERS.VISU": "Visualisation des écoutes de l'utilisateur sur la plateforme Divercities.",
                  "USERS_AND_INVITATIONS": "Comptes actifs et invitations envoyées",
                  "VALIDATE": "Valider",
                  "VALIDITY_DUE_DATE": "Date de fin de validité",
                  "VALIDITY_DUE_DATE.HINT": "Si cette date n'est pas vide, le compte utilisateur expirera passée cette date.",
                  "VIDEOS": "Vidéos",
                  "WAIT_SIGNOUT": "Déconnexion, veuillez patienter...",
                  "WELCOME": "Bienvenue !",
                  "YOUR_PROFILE": "Votre profil",
                  "ZIP_CODE": "Code postal",
              });

          $translateProvider.translations('en',
              {
                  "ACCESS_RESOURCE": "Access resource",
                  "ACCESS_DENIED": "Access denied",
                  "ACTION_NOW": "Read now",
                  "ACTIVE": "Active",
                  "ADD": "Add",
                  "ADD: {{ item_type | translate }}": "Add: {{ item_type | translate }}",
                  "ADD_DESC": "Add a description",
                  "ADD_TO_PLAYLIST": "Add to a playlist",
                  "ADD_TO_STOCK": "Add to stock",
                  "ADDRESS": "Address",
                  "ALL": "All",
                  "ASK_FOR_PROMOTION": "Ask for promotion",
                  "ASSISTANTS": "Assistants",
                  "BACK": "Back",
                  "BACK_TO_SIGN_UP_FORM": "Back to signup form",
                  "BOOK": "Book",
                  "BROWSE": "Browse",
                  "BROWSE_WITH_GENRE": "Browse: {{ genre }}",
                  "CANCEL_ASK_FOR_PROMOTION": "Cancel ask for promotion",
                  "CAPSULES": "Capsules",
                  "CHANGE_LEVEL": "Change level",
                  "STATIONS.CHILDREN": "Child stations",
                  "CHOOSE_NEW_LEVEL": "Choose new level",
                  "CHOOSE_NEW_STATION": "Choose new station",
                  "CITY": "City",
                  "CONFIRM": "Confirm",
                  "CONFIRM_DELETE_QUESTION": "Are you sur you want to remove this item ?",
                  "CONTACT_US": "Contact us",
                  "CONTINUE": "Continue",
                  "COUNTRY": "Country",
                  "COVER": "Main image",
                  "COVER_PLACEHOLDER": "Main image here",
                  "CREATED": "Created",
                  "CREATED_AT": "Created",
                  "UPDATED_AT": "Updated",
                  "RENEWED_AT": "Renewed",
                  "LAST_SIGN_IN": "Last sign in",
                  "CREATION_YEAR": "Creation year",
                  "CREATION_YEAR_PLACEHOLDER": "Creation year here",
                  "CREATIVE_FIELDS": "Creative fields",
                  "CREATIVE_FIELDS_INVITATION": "Invitation to creative field(s)",
                  "CREATIVE_FIELDS_MUNKI_CREATION": "Create a Munki account",
                  "CREATIVE_FIELDS.HINT": "Invitation will be sent by email",
                  "CREATIVE_FIELDS_MUNKI.HINT": "Application Connexion parameters and download link will be sent by email",
                  "CREATIVE_FIELD_MISSING": "This creative field is not activated for this user's station",
                  "CREATIVE_FIELD.NOT_ACTIVATED": "This creative field is not activated for your station. Please contact our commercial team to via the Services page to ask for activation.",
                  "CREATIVE_FIELD.UNACTIVABLE": "This creative field is not yet available but will be soon. Please contact our commercial team to via the Services page to get more information.",
                  "CSV_FILE": "CSV file",
                  "DASHBOARD": "Dashboard",
                  "DATA_PROCESSED": "Data processed :)",
                  "DELETED": "Deletd",
                  "DESCRIPTION": "Description : {{ lang }}",
                  "DESCRIPTION": "Description",
                  "DESCRIPTION_PLACEHOLDER": "Description...",
                  "DEVELOPERS": "Developers",
                  "DEVELOPERS_PLACEHOLDER": "Developers here",
                  "DISABLED": "Disabled",
                  "DISCONNECTED_MSG": "You're now disconnected.\n" +
                                      "Thanks for your visit.",
                  "DISCOVER": "Discover",
                  "DISCOVER_NEW_AUTHORS": "What about discovering new authors?",
                  "DISTRIBUTOR": "Distributor",
                  "DISTRIBUTOR_PLACEHOLDER": "Distributor here",
                  "DIVERCITIES": "Divercities",
                  "DOWNLOAD": "Download",
                  "DOWNLOADABLE_GAMES": "Downloadable games",
                  "DOWNLOADS_LIMIT_REACHED": "Downloads limit reached",
                  "DROP_FILE_HERE": "Drop file here",
                  "DROP_FILES_HERE": "Drop files here",
                  "EDIT": "Edit",
                  "EDIT_GAME": "Edit: <em>{{ item }}</em>",
                  "EDIT_WORK": "Edit: <em>{{ work }}</em>",
                  "EDITORS": "Editors",
                  "EMAIL_ADDRESS": "Email address",
                  "EMAIL.INVALID": "The email format is invalid.",
                  "EMAIL_ADDRESS_PLACEHOLDER": "Email address...",
                  "ENABLED": "Enabled",
                  "EVERYONE": "Everyone",
                  "EXPIRED": "Expired",
                  "FAIR_STREAMING": "Fair streaming",
                  "FILTER": "Filter",
                  "FILTER_PLACEHOLDER": "Filter...",
                  "FIRST_NAME": "First name",
                  "FOR_STATION": "for {{ station_name }}",
                  "FORGOTTEN_PASSWORD": "Forgotten password ?",
                  "FORK": "Import",
                  "FORKED_N_TIMES": "Imported {{ n }} times",
                  "FORM.BAD_VALUES": "Some fields have bad values",
                  "FROZEN": "Frozen",
                  "GAME": "Video game",
                  "GAME_APP": "Divercities Game",
                  "GAME_PITCH": "Pitch",
                  "GAME_REQUIREMENTS": "Requirements",
                  "GAMES_STUDIO_MADE": "Games",
                  "GENRES": "Genres",
                  "GENRES": "Genres",
                  "GENRES_PLACEHOLDER": "Genres here",
                  "GOTO_DETAILS": "Go to details",
                  "HELP": "Help",
                  "IMAGES": "Images",
                  "IMAGES_PLACEHOLDER": "Drop images here...",
                  "IN_STOREROOM": "In Storeroom",
                  "INDIE_CREATION": "Indie creation",
                  "INDIE_CREATIVES": "Indie creatives",
                  "INVITED": "User's account creation is not complete",
                  "INVITED_AT": "Invited",
                  "ITEM_CREATED": "{{ item_type | translate }} created",
                  "ITEM_ERROR": "Error while processing item",
                  "KEY": "Key",
                  "KEYSTEAM": "Steam key",
                  "LAST_CONNECTED_AT": "Last connection",
                  "LAST_NAME": "Last name",
                  "LATEST_GAMES_STUDIO_MADE": "Latest releases",
                  "LETS_SIGNUP": "Welcome!\n" +
                                 "Let's create your Divercities account",
                  "LEVEL": "Level",
                  "LEVEL_PLACEHOLDER": "Level...",
                  "LIMIT_REACHED": "Limit reached",
                  "LOGIN": "Login",
                  "LOGIN_PLACEHOLDER": "Login",
                  "LOGO": "Logo",
                  "LOGO_PLACEHOLDER": "Logo here",
                  "LOGOUT": "Logout",
                  "MANAGERS": "Managers",
                  "MEMBERS": "members",
                  "MONTHS": "Months",
                  "MOVE_USER": "Move user",
                  "MRA_NUMBER": "Your M'ra number",
                  "MRA_PASSWORD": "Your M'ra password",
                  "MRA_PASSWORD_FORGOTTEN": "M'ra password forgotten?",
                  "MRA_PASSWORD_HINT": "By default, your birthdate \"DD/MM/YYYY\"",
                  "MUNKI_PROVIDER_MISSING": 'A Munki Provider ID should be provided to activate Munki as a creative field.',
                  "MUSIC": "Music",
                  "MUSIC_APP": "Divercities Music",
                  "MUNKI": "Munki",
                  "MY_ACCOUNT": "My account",
                  "MY_CAPSULES": "My capsules",
                  "MY_GAME_LIBRARY": "My library",
                  "MY_GAMES": "My games",
                  "MY_LIBRARY": "My pocket",
                  "MY_MUSICS": "My music",
                  "MY_PLACE": "My place",
                  "N_FORKS": "{{ n }} forks",
                  "NAME": "Name",
                  "NAME_PLACEHOLDER": "Name...",
                  "NB_ITEMS": "{{ nb }} items",
                  "NEW_PLAYLIST": "New playlist",
                  "NEW_STOCK": "New stock",
                  "NEW_THINGS": "New things",
                  "NO_CAPSULE": "No capsule for now...",
                  "NO_RESULTS": "No results",
                  "NONE": "None",
                  "OAUTH_CLIENT_ID": "Client ID",
                  "OAUTH_CLIENT_SECRET": "Client secret",
                  "OAUTH_CLIENT_OPTIONS": "Client options",
                  "OAUTH_SCOPE": "Scope",
                  "OFFERED": "Offered",
                  "PARAMETERS": "Parameters",
                  "PARENT_ID": "Parent station id",
                  "PASSWORD": "Password",
                  "PHONE.INVALID": "The phone number is invalid.",
                  "PLACES_1D_TOUCH": "Places Divercities",
                  "PROCESSING_ERROR": "Error while processing your request",
                  "PRODUCER": "Producer",
                  "PRODUCER_PLACEHOLDER": "Producer here",
                  "PROMOTE": "Promote",
                  "PROMOTED": "Promoted",
                  "RATING": "Rating",
                  "RATING_HINT_BAD": "Bad",
                  "RATING_HINT_GOOD": "Good",
                  "RATING_HINT_GORGEOUS": "Gorgeous",
                  "RATING_HINT_POOR": "Poor",
                  "RATING_HINT_REGULAR": "Regular",
                  "RE_INVITE": "Re-invite",
                  "REDACTOR_HINT": "Sélectionner du texte pour le mettre en forme",
                  "RELEASE_YEAR": "Release year",
                  "RELEASE_YEAR_PLACEHOLDER": "Release year here",
                  "RETRIEVE_MRA_PASSWORD_HINT": "Fill in this form to retrieve your M'ra password",
                  "SAVE": "Save",
                  "SAVE_MODIFICATIONS": "Save",
                  "SCREENSHOTS": "Screenshots",
                  "SCREENSHOTS_PLACEHOLDER": "Drop screenshots here...",
                  "SEARCH": "Search",
                  "SEARCH_PLACEHOLDER": "Search...",
                  "SEND": "Send",
                  "SERVICES": "Services",
                  "SERVICES.SUBTITLE": "All your services put together in the Kiosque",
                  "SERVICES.CONTACT_REQUEST_SUBMIT": "Request for contact",
                  "SERVICES.CONTACT_REQUEST_MORE": "More informations on ",
                  "SERVICES.CONTACT_REQUEST_TEXT": "Need help? Contact our team!\n" + 
                                                   "Please fill in the e-mail and/or phone number you want to be reached at.",
                  "SERVICES.STATS_REQUEST_TEXT": "Claim your statistics report.",
                  "SERVICES.NOT_ACTIVATED": "You didn't subscribe for this service yet ?",
                  "SERVICES.FIELD_REQUIRED": "One of the two fields is required",
                  "SERVICES.RGPD.TITLE": "RGPD",
                  "SERVICES.RGPD.LINK": "Read the dedicated page",
                  "SERVICES.DOCUMENTATION.TITLE": "Documentation",
                  "SERVICES.DOCUMENTATION.LINK": "Discover all our ressources",
                  "SIGN_IN": "Sign in",
                  "SIGN_OUT": "Sign out",
                  "SIGN_UP": "Sign Up",
                  "SIGNUP_UNAVAILABLE": "Signup is temporarily unavailable",
                  "SIMILAR_GAMES": "Similar games",
                  "STATION": "Station",
                  "STATION_ID": "Station ID",
                  "STATION_MEMBERS": "{{ count }} members",
                  "STATIONS": "Stations",
                  "STATIONS.SSO_LINK": "SSO Link",
                  "STATIONS.STOP": "Shutdown station",
                  "STATIONS.STATS": "Statistics",
                  "STATIONS.STATS.FACTOR": "Factor",
                  "STATIONS.SUBTITLE": "All the Divercities stations",
                  "STATIONS.UI": "UI",
                  "STATIONS.UI.HINT": "diMusic user interface version",
                  "STATIONS.CHECK_PORTAL_ACCOUNT_VALIDITY.HINT": "Check portal account validity: prevent user from downloading if account has not been validated at portal side.",
                  "STATIONS.SEARCH_ALL": "Search in children stations",
                  "STATIONS.ACTION": "Action",
                  "STATIONS.ACCESS": "Access",
                  "STATIONS.ACTIVE_ACCOUNTS": "Active",
                  "STATIONS.ADD_PLACE": "Add a place",
                  "STATIONS.ADD_STATION": "Add a place",
                  "STATIONS.AFFILIATE_TO": "Affiliate to",
                  "STATIONS.ASSOCIATE_CLIENT": "Associate client",
                  "STATIONS.AUTHORIZE_ACCESS": "Grant access",
                  "STATIONS.AUTHORIZE_HANDLING": "Allow managers to handle account validity",
                  "STATIONS.CITY": "City",
                  "STATIONS.CLIENT": "Client settings",
                  "STATIONS.CLIENT.MUNKI_PROVIDER_ID": "Associated Munki provider id",
                  "STATIONS.CLIENT.TENK_GROUP_ID": "Associated Tënk group id",
                  "STATIONS.CLIENT.NAME": "Name",
                  "STATIONS.CODES": "Invitation codes",
                  "STATIONS.CODES_ALL": "All codes",
                  "STATIONS.CODES_DESCRIPTION": "Invitation coupons are single-use codes that allow a user to sign up on Divercities.\n" +
                                                "Once a coupon has been generated, it can be used on the page <a ng-href=\"{{use_url}}\">{{use_url}}</a>. If the code is validated, the user will be asked to choose its password and sign in to Divercities within your station.\nA coupon can only be used once. Once used, it becomes obsolete and won't be usable to create any account again.",
                  "STATIONS.CODES_FEATURES": "Code features",
                  "STATIONS.CODES_USED": "Code state",
                  "STATIONS.CODES_QUOTA": "Codes générés",
                  "STATIONS.CODES_SERIAL": "Serial",
                  "STATIONS.CODES_SERIALS": "Code serials",
                  "STATIONS.CODES_SERIAL_NAME": "Serial name",
                  "STATIONS.CODES_SERIAL_NUMBER": "Serial number",
                  "STATIONS.CODES_SERIAL_COUNT": "Codes total",
                  "STATIONS.CODES_SERIAL_FEATURES": "Features of the codes",
                  "STATIONS.CODES_VALUE": "Code value",
                  "STATIONS.CODES.FILTER": "Filter",
                  "STATIONS.CODES.REMAINING_SERIAL": "Nombre de code non utilisés / Nombre de code total dans la série",
                  "STATIONS.COMMON.SETTINGS.AUTHORIZE_STATION_TO_GENERATE_CODES": "Authorize station to generate codes",
                  "STATIONS.COMMON.SETTINGS.AUTHORIZE_STATION_TO_USE_CODES": "Authorize station to use codes",
                  "STATIONS.COMMON.SETTINGS": "Common parameters",
                  "STATIONS.CONTRACT_DATE": "Contract start date",
                  "STATIONS.COUNTRY": "Country",
                  "STATIONS.DEFAULT_CREATIVE_FIELDS": "Default creative fields",
                  "STATIONS.DEFAULT_CREATIVE_FIELDS.HINT": "Automatically authorized creative fields for new users",
                  "STATIONS.DESCRIPTION": "Manage your community, browse your stats.",
                  "STATIONS.FORCE_ALL_CREATIVE_FIELDS_ACCESS": "Authorize access on demand",
                  "STATIONS.FORCE_ALL_CREATIVE_FIELDS_ACCESS.HINT": "Authorize user to access creative field even if his account is not checked",
                  "STATIONS.FROZEN_ACCOUNTS": "Frozen accounts",
                  "STATIONS.GAME.SETTINGS": "Videogames parameters",
                  "STATIONS.GENERATE_CODES.CREATIVE_FIELDS": "Creative fields",
                  "STATIONS.GENERATE_CODES.CREATIVE_FIELDS.HINT": "Creative fields allowed for the user of the code",
                  "STATIONS.GENERATE_CODES.FEATURES": "Features",
                  "STATIONS.GENERATE_CODES.LABEL": "Number of codes to generate",
                  "STATIONS.GENERATE_CODES.MAX_DOWNLOADS": "Download counts (game)",
                  "STATIONS.GENERATE_CODES.MAX_DOWNLOADS.HINT": "the maximal number of videogames the user of the coupon can download",
                  "STATIONS.GENERATE_CODES.VALIDITY": "Account validity duration",
                  "STATIONS.GENERATE_CODES.VALIDITY.HINT": "The validity duration of the account of the user of the code (in days)",
                  "STATIONS.GENERATE_CODES.INCLUDE_SERIAL": "Include codes in a serial",
                  "STATIONS.GENERATE_CODES.SERIAL_NAME": "Serial name",
                  "STATIONS.GENERATE_CODES": "Generate codes",
                  "STATIONS.GENERATED_CODES": "Generated codes",
                  "STATIONS.HAS_VALIDITY_DUE_DATE": "Activate the use of a validity due date for the users of this station",
                  "STATIONS.ID": "ID",
                  "STATIONS.STATUS": "Status",
                  "STATIONS.INFORMATIONS": "Information",
                  "STATIONS.INVITE_SOMEONE": "Invite someone",
                  "STATIONS.INVITED_ACCOUNTS": "Invited",
                  "STATIONS.LOGO_INFO": "Current logo",
                  "STATIONS.MANAGER_HANDLE_VALIDITY": "Account validity handling by managers",
                  "STATIONS.MANAGER_HANDLE_VALIDITY.HINT": "This allow managers to handle the station's users validity by themselves.",
                  "STATIONS.MASS_INVITE": "Invite some people",
                  "STATIONS.MAX_STATION_DOWNLOADS": "Max station downloads",
                  "STATIONS.MAX_USER_DOWNLOADS": "Max unique games downloadable per user",
                  "STATIONS.MAX_USER_DOWNLOADS.HINT": "Maximum number of games the member can download (contractually limited).",
                  "STATIONS.MUSIC.SETTINGS": "Music parameters",
                  "STATIONS.NAME": "Name",
                  "STATIONS.PERIOD": "Period",
                  "STATIONS.REMAINING_CODES": "Remaining codes",
                  "STATIONS.SETTINGS": "{{ creative_field | capitalize }} parameters",
                  "STATIONS.SUBSCRIPTION": "Abonnement",
                  "STATIONS.SSO_SETTING": "Customer parameters",
                  "STATIONS.SSO_SETTING.BIBID": "BibID",
                  "STATIONS.SSO_SETTING.CHECK_USER_VALIDITY_AT_SIGNIN": "Check account validity when he signs in",
                  "STATIONS.SSO_SETTING.PORTAL_URL": "Portal URL",
                  "STATIONS.SSO_SETTING.PROVIDER": "CMS",
                  "STATIONS.SSO_SETTING.USER_CAN_CHOOSE_PASSWORD": "User has to choose a password",
                  "STATIONS.UNAFFILIATE": "Unaffiliate",
                  "STATIONS.UNLIMITED": "Unlimited",
                  "STATIONS.USED_CODES": "Used codes",
                  "STATIONS.VALIDITY_DUE_DATE": "Validity due date",
                  "STATIONS.VALIDITY_DUE_DATE.HINT": "This validity due date defines the validity of ALL the station's accounts. \n\n Let this date empty if you want to handle the account's validity independantly at each account creation.",
                  "STATIONS.VALIDITY_DURATION": "User accounts validity duration",
                  "STATIONS.WELCOME_TEXT": "Welcome text",
                  "STATIONS.WELCOME_TEXT.HINT": "This text will be sent when new user create an account in this station. Let this field empty to use a standard text.",
                  "STATIONS.INVITATION_TEXT": "Invitation text'",
                  "STATIONS.INVITATION_TEXT.HINT": "This texte will be sent when a manager invites a user in this station. Let this field empty to use a standard text.<br><br>" +
                                                      "You can also use the following variables : [[user]] (username) et [[email]] (user email)",
                  "STATIONS_INVITATION_MESSAGE_LABEL": "This text will be followed by a button, so that user can accept invitation",
                  "STATS": "Statistics",
                  "STATS.SUBTITLE": "Your services analytics",
                  "STATS.ALL_STATIONS": "All stations",
                  "STATS.COMPARISON_PERIOD": "Comparison period",
                  "STATS.DOWNLOAD.HINT": '<div class="title">How to open the CSV file</div><div class="description"><dt>LibreOffice/GoogleSheet</dt><dd>Select UTF-8 encoding<br/>Select <code>;</code> (semi-colon) separator</dd><dt>Excel</dt><dd>Detection is automatic</dd></div>',
                  "STATS.EXPORT": "Export",
                  "STATS.EXPORTING": "Exporting...",
                  "STATS.GA3": "Till 2023, July 1st",
                  "STATS.GA4": "From 2023, July 1st",
                  "STATS.LOADING": "Loading",
                  "STATS.NB_RESULTS": "{{nb}} results",
                  "STATS.NEW_SEGMENT": "New segment",
                  "STATS.NO_ACCESS": "You don't have access to analytics because the chosen service is not activated for yout station.<br><br>" +
                                     "You can ask our commercial team to activate this service in the Services page.",
                  "STATS.ACCESS_SERVICES": "Go to the services page",
                  "STATS.NO_SEGMENT": "No segment",
                  "STATS.NODATA": "No data available",
                  "STATS.PERIOD": "Period",
                  "STATS.REMOVE_ALL": "Remove all",
                  "STATS.SAMPLE": "User Sample",
                  "STATS.SEGMENT": "Segment",
                  "STATS.STATION": "Station",
                  "STATS.USER_SAMPLE": "User sample",
                  "STEAM_PLACEHOLDER": "Steam key...",
                  "STOCK": "Stock",
                  "STOREROOM": "Storeroom",
                  "STUDIO": "Studio",
                  "STUDIO_PLACEHOLDER": "Studio here",
                  "STUDIOS": "Studios",
                  "STUDIOS_PLACEHOLDER": "Studios here",
                  "SUBSCRIBE_CAPSULES": "Subscribe capsule",
                  "TENK": "Tënk",
                  "TENK_PROVIDER_MISSING": 'A Tenk Group ID should be provided to activate Tënk as a creative field.',
                  "TITLE": "Title",
                  "TITLE_PLACEHOLDER": "Title here",
                  "TOS": "GTU",
                  "TOURS_ADMIN": "Tours administration",
                  "TV_APP": "Divercities TV",
                  "UNAUTHORIZED_MSG": "You're not authorized on {{ app }}.\n" +
                                      "Please ask your manager.",
                  "UNPROMOTE": "Unpromote",
                  "UPDATE": "Update",
                  "USER": "User",
                  "USER.INFORMATIONS": "Informations",
                  "USER.CREATIVE_FIELDS": "Creative fields access",
                  "USER.CREATIVE_FIELD_VALIDITY_DUE_DATE": "Access validity due date",
                  "USER.CREATIVE_FIELD_MAX_USER_DOWNLOADS": "Max videogame downloads",
                  "USER.OFFERS": "Used offers",
                  "USER.OFFERS_ALL": "All used offers",
                  "USERS": "Users",
                  "USERS.SUBTITLE": "All the users and their services",
                  "USERS.ACTION": "Action",
                  "USERS.ACTIVE_ACCOUNTS": "Active accounts",
                  "USERS.CLEAN": "Clean",
                  "USERS.CONTRIB": "Creative contribution",
                  "USERS.DELETE": "Delete",
                  "USERS.DELETED": "Supprimé",
                  "USERS.DESCRIPTION": "Manage your community, browse your stats.",
                  "USERS.EXPIRED": "Expired",
                  "USERS.EXPORT": "Export",
                  "USERS.EXPORT.TOOLTIP": "Export current view",
                  "USERS.FREEZE": "Freeze",
                  "USERS.FREEZE_FROM": "Freeze user accounts which have been inactive for",
                  "USERS.FROZEN_ACCOUNTS": "Frozen accounts",
                  "USERS.FROZEN": "User account is frozen",
                  "USERS.ID": "ID",
                  "USERS.INVITATIONS_SENT": "Invitations sent",
                  "USERS.CREATE": "Create account",
                  "USERS.CREATE.MULTIPLE": "Create several accounts",
                  "USERS.INVITE": "Invitation",
                  "USERS.INVITE.MULTIPLE": "Multiple invitations",
                  "USERS.ACTIVITY": "Activity",
                  "USERS.LAST_ACTIVITY": "Last activity",
                  "USERS.LAST_LISTENING": "Last listening",
                  "USERS.LAST_SIGN_IN": "Last sign-in",
                  "USERS.LEVEL": "Level",
                  "USERS.LISTENING": "Listening",
                  "USERS.MONTHS": "month(s)",
                  "USERS.NAME": "Name",
                  "USERS.NEVER": "Never",
                  "USERS.REINVITE": "Re-invite",
                  "USERS.REINVITE.TOOLTIP": "Re-send all invitations",
                  "USERS.REINVITE_CONFIRM": "Re-invite all waiting accounts, for station <em>{{station.name}}</em> ?",
                  "USERS.REINVITE_UNAVAILABLE": "Impossible to re-invite without a specific station",
                  "USERS.REMOTE_ACCOUNTS": "Remote accounts",
                  "USERS.REMOTE_FREEZE": "Freeze remote account",
                  "USERS.REMOTE_UNFREEZE": "Unfreeze remote account",
                  "USERS.REMOTE_REINVITE": "Re-send signin instructions for {{ service }}",
                  "USERS.RENEW": "Renew account",
                  "USERS.SHOW": "Show details",
                  "USERS.STATION": "Station",
                  "USERS.STATUS": "Status",
                  "USERS.TRANSFER": "Transfer",
                  "USERS.UI": "UI",
                  "USERS.UI.HINT": "diMusic user interface version",
                  "USERS.UNFREEZE": "Unfreeze",
                  "USERS.UPDATE": "Update name/email",
                  "USERS.UPDATE_VALIDITY": "Update validity",
                  "USERS.UPDATE_SETTINGS": "Update settings",
                  "USERS.VISU": "Divercities listening visualization",
                  "USERS_AND_INVITATIONS": "Active accounts and invitations sent",
                  "VALIDATE": "Validate",
                  "VALIDITY_DUE_DATE": "Validity due date",
                  "VALIDITY_DUE_DATE.HINT": "If this date is not empty, the user's account will expire after this date.",
                  "VIDEOS": "Video",
                  "WAIT_SIGNOUT": "Please wait while signing out",
                  "WELCOME": "Welcome!",
                  "YOUR_PROFILE": "Your profil",
                  "ZIP_CODE": "Postal code",
              });

          $translateProvider.preferredLanguage((navigator.language !== null ? navigator.language : navigator.browserLanguage).split("_")[0].split("-")[0] || 'fr');
          $translateProvider.fallbackLanguage('en');
          $translateProvider.useSanitizeValueStrategy(null);
      }]);
})();
;(function() {
	"use strict";

	var app = angular.module('app.core')
	.directive('lazyLoad', ['$window', '$timeout', '$rootScope', function($window, $timeout, $rootScope) {
		return {
			restrict: 'EA',

			//child scope instead of isolate scope because need to listen for ng-include load events from other scopes
			scope: true,
			link: function(scope, element, attrs) {
				var elements = [],
					threshold = Number(attrs.threshold) || 0;

				//gets all img(s), iframe(s) with the 'data-src' attribute and changes it to 'src'
				function getElements() {
					// fetch all image elements inside the current element
					// fetch all iframe elements inside the current element
					// fetch all divs inside the current element
					elements =  Array.prototype.slice.call(element[0].querySelectorAll('img[data-src], iframe[data-src], div[data-src]'));
					//if images and videos were found call loadMedia
					if(elements.length > 0 ) {
                        elements.map(resizeContainer);
						loadMedia();
					}
				}

				//replaces 'data-src' with 'src' for the elements found.
				function loadMedia() {
					console.log('get elements', elements);
					elements = elements.reduce(function ( arr, item ) {
						var src = item.getAttribute("data-src");

						if (!$(item).is(':visible')) {
							arr.push(item);
							return arr;
						}

						console.log('get elements, item:', item);
						switch(item.tagName) {
							case "IMG":
							case "IFRAME":
								item.src = src;
								break;
							case "DIV":
								item.style.backgroundImage = "url("+src+")";
								break;
							default:
								arr.push(item);
						}

						return arr;
					}, []);
				};

				getElements();

				function reloadElements (ev) {
					// console.log('reload elements', ev);
					$timeout(getElements, 100);
				}

				function reloadMedia ( ev ) {
					// console.log('reload media', ev);
					$timeout(loadMedia, 100);
				}

                function resizeContainer(elem) {
                    // console.log('will resize on iframe loaded', elem);
                    $(elem).unbind('load', timeoutResize);
                    $(elem).bind('load', timeoutResize);
                }
                function timeoutResize(ev) {
                    $(ev.target).parents('#dashboard-view .container').css({width: 'auto'});
                    $timeout(function() {
                        // console.log('resize on iframe loaded', ev);
                        $(ev.target).parents('#dashboard-view .container').css({width: 'calc(100% - 1.1em'});
                    }, 2500);
                }

				//listens for partials loading events using ng-include
				scope.$on('$includeContentLoaded', reloadElements);

				//listens for selective loading, that is, if the developer using this directive wants to load the elements programmatically he can emit a selectiveLoad event
				scope.$on('selectiveLoad', reloadElements);
				scope.$on('selectiveLoadMedia', reloadMedia);

				//calls loadMedia for each window scroll event
				scope.$on('mousewheel', reloadMedia);

				//calls loadMedia for each window scroll event
				angular.element($window).bind('resize', reloadMedia);
			}
		}
	}]);
})();
/* globals APP, itemsDummy*/

(function () {
    'use strict';

    angular
        .module('app.core')
        .service('MainService', mainService);

    mainService.$inject = ['$http', '$q', 'logger', '$location', '$translate', 'exception', '$injector'];
    /*  */
    function mainService($http, $q, logger, $location, $translate, exception, $injector) {
        var bodyClass = '';
        var apps = [];
        var appsSlug = [];

        var service = {
            fetchApps: fetchApps,
            bodyClass: bodyClass,
            lang: $translate.use,
            preferredLang: $translate.preferredLanguage,
            models: getModels,
            baseUrl: getBaseUrl,
            accountsBaseUrl: getAccountsBaseUrl,
            statsBaseUrl: getStatsBaseUrl,
            apiEndpoint: getApiEndpoint,
            commonApiEndpoint: getCommonApiEndpoint,
            statsApiEndpoint: getStatsApiEndpoint,
            stationsEndpoint: getStationsApiEndpoint,
            app: getApp,
            apps: getApps,
            appsSlug: getAppsSlug,
            APP: APP,
            isTouchDevice: isTouchDevice
        };
        
        return service;



        //////////////////////

        function fetchApps() {
          if ($injector.get('myservice').me()) {
            var deferred = $q.defer();
            $http.get(getCommonApiEndpoint() + 'creative_fields')
              .then(function(response) {
                apps = response.data || [];
                appsSlug = apps.map(function(service) {
                  return service.slug;
                });
                deferred.resolve(apps);
                return apps;
              }).catch(function(error) {
                deferred.reject(error);
                return exception.catcher('Error while fetching services list ('+getCommonApiEndpoint() + 'creative_fields'+')')(error);
              });
            return deferred.promise;
          }
        }

        function getApps() {
          // var deferred = $q.defer();
          return apps;
        }


        function getAppsSlug() {
          // var deferred = $q.defer();
          return appsSlug;
        }

        // function getAppsSlug() {
        //   var deferred = $q.defer();
        //   if (appsSlugs.length == 0) {
        //     $http.get(service.commonApiEndpoint() + 'creative_fields/slugs')
        //       .then(success)
        //       .catch(fail);

        //     function success(response) {
        //       appsSlugs = response.data
        //       deferred.resolve(response.data);
        //     }

        //     function fail(e) {
        //       deferred.reject('XHR Failed for getAppsSlugs ' + e);
        //     }
        //   } else {
        //     deferred.resolve(appsSlugs);
        //   }
        //   return deferred.promise.catch(angular.noop);

        // }

        function getApp(currentApp) {
          if (appsSlug.indexOf(currentApp) == -1) {
            currentApp = (window.location.hostname.split('.')[0]).replace(/^terminal-/, '');
          }
          if (appsSlug.indexOf(currentApp) == -1) {
            currentApp = '';
          }

          return currentApp;
        };

        // touchdevice
        function isTouchDevice() {
          var host = window.location.hostname.split('.')[0];
          return (host.indexOf('terminal-') > -1);
        }

        function getAccountsBaseUrl(currentApp) {
          return (DEV_DC_DOMAIN || (SCHEME + 'accounts.' + DC_DOMAIN + (PORT?':'+PORT:''))) + '/';
        }

        function getStatsBaseUrl(currentApp) {
          return (DEV_DC_DOMAIN || (SCHEME + 'stats.api.' + DC_DOMAIN + (PORT?':'+PORT:''))) + '/';
        }

        function getBaseUrl(currentApp) {
          currentApp = getApp(currentApp);
          switch (currentApp) {
            default:
              return (DEV_DC_DOMAIN || (SCHEME + (currentApp?currentApp+'.' : '') + 'api.' + DC_DOMAIN + (PORT?':'+PORT:''))) + '/';
          }
        }
        function getCommonApiEndpoint() {
            return (DEV_DC_DOMAIN || (SCHEME + 'api.' + DC_DOMAIN + (PORT?':'+PORT:''))) + '/' + (DEV_PATH_PREFIX || '') + VERSION + '/';
        }
        function getApiEndpoint(currentApp) {
          currentApp = getApp(currentApp);
          switch (currentApp) {
            case 'terminal-music':
              return getBaseUrl() + 'api/' + VERSION + '/';

            default:
              return getBaseUrl(currentApp) + (DEV_PATH_PREFIX || '') + VERSION + '/';
          }
        }
        function getStatsApiEndpoint(currentApp) {
          return getStatsBaseUrl() + (DEV_PATH_PREFIX || '') + VERSION + '/';
        }
        function getStationsApiEndpoint(currentApp) {
          currentApp = getApp(currentApp);
          switch (currentApp) {
            case 'terminal-music':
            return getBaseUrl() + 'api/' + VERSION + '/stations';

            default:
            return getBaseUrl() + (DEV_PATH_PREFIX || '') + VERSION + '/stations';
          }
        }

        function getModels(currentApp) {
          currentApp = getApp(currentApp);
          switch (currentApp) {
            case 'terminal-game' :
            case 'game' :
              return {
                promoted :{model: 'selection', type: 'capsule',   path: 'promoted',   endpoint: 'promoted'},
                capsules: {model: 'selection', type: 'capsule',   path: 'capsules',   endpoint: 'selections'},
                works:    {model: 'game',      type: 'game',      path: 'games',      endpoint: 'games', id_name: 'videogame_id'},
                authors:  {model: 'studio',    type: 'studio',    path: 'studios',    endpoint: 'studios'},
                homes:    {model: 'producer',  type: 'producer',  path: 'producers',  endpoint: 'producers'},
                platforms:{model: 'platform',  type: 'platform',  path: 'platforms',  endpoint: 'platforms'},
                genres:   {model: 'genre',     type: 'genre',     path: 'genres',     endpoint: 'genres'},
                tours:    {model: 'tour',      type: 'tour',      path: 'tours',      endpoint: 'tours'}
              };

            case 'book' :
              return {
                promoted :{model: 'selection', type: 'capsule',   path: 'promoted',   endpoint: 'promoted'},
                capsules: {model: 'selection', type: 'capsule',   path: 'capsules',   endpoint: 'selections'},
                works:    {model: 'book',      type: 'book',      path: 'books',      endpoint: 'books', id_name: 'book_id'},
                authors:  {model: 'author',    type: 'author',    path: 'authors',    endpoint: 'authors'},
                homes:    {model: 'publisher', type: 'publisher', path: 'publishers',    endpoint: 'publishers'},
                platforms:{model: 'platform',  type: 'platform',  path: 'platforms',  endpoint: 'platforms'},
                genres:   {model: 'genre',     type: 'genre',     path: 'genres',     endpoint: 'genres'},
                readings: {model: 'reading',   type: 'reading',   path: 'readings',   endpoint: 'readings'},
                tours:    {model: 'tour',      type: 'tour',      path: 'tours',      endpoint: 'tours'},
                playlists: {model: 'readlist', type: 'readlist',  path: 'readlists',  endpoint: 'readlists'}
              };

            case 'terminal-music' :
            case 'music' :
              return {
                promoted :{model: 'selection', type: 'capsule',   path: 'promoted',   endpoint: 'promoted'},
                capsules: {model: 'selection', type: 'capsule',   path: 'capsules',   endpoint: 'selections'},
                works:    {model: 'album',     type: 'album',     path: 'albums',     endpoint: 'albums', id_name: 'allbum_id'},
                tracks:   {model: 'track',     type: 'track',     path: 'tracks',     endpoint: 'tracks'},
                authors:  {model: 'artist',    type: 'artist',    path: 'artists',    endpoint: 'artists'},
                homes:    {model: 'label',     type: 'label',     path: 'labels',     endpoint: 'labels'},
                platforms:{model: 'platform',  type: 'platform',  path: 'platforms',  endpoint: 'platforms'},
                genres:   {model: 'genre',     type: 'genre',     path: 'genres',     endpoint: 'genres'},
                tours:    {model: 'tour',      type: 'tour',      path: 'tours',      endpoint: 'tours'},
                playlists: {model: 'playlist', type: 'playlist',  path: 'playlists',  endpoint: 'playlists'}
              };
            }
        };
    }
})();

(function () {
    'use strict';

    angular
        .module('app.core')
        .factory('myservice', myservice);

    myservice.$inject = ['$http', '$q', '$localStorage', 'exception', 'logger', 'MainService', '$rootScope'];
    /*  */
    function myservice($http, $q, $localStorage, exception, logger, MainService, $rootScope) {
        var loading = false;
        var service = {
            getMe: getMe,
            isAdmin: isAdmin,
            isAuthed: isAuthed,
            me: me,
            signOut: signOut
        };

        var empty = {id: null, station: null, level: null};
        // getMe();

        return service;

        function success(response) {
            loading = false;
            console.log('getMe()', response);
            if (response.status >= 200 && response.status < 300) {
                $localStorage.me = response.data;
                console.log('me got stored', $localStorage.me);
                return response.data;
            } else {
                return null;
            }
        }
        function fail(e) {
            loading = false;
            console.log('XHR Failed for getMe');
            return null;
        }

        function getMe() {
            loading = true;
            return isAuthed() && $http.get(MainService.apiEndpoint()+'me').then(success, fail);
        }

        function me() {
            var me = $localStorage.me;
            if (me && me.id) {
                return me;
            } else {
                // getMe().then(success, fail);
                return null;
            }
        }

        function isAdmin() {
            if ($localStorage.me) {
                return $localStorage.me.level == 'admin';
            } else {
                return false;
            }            
        }

        function isAuthed() {
            return $localStorage.user && $localStorage.user.token;
        }

        function signOut() {
            $localStorage.$reset();
        }
    }
})();

(function () {
    'use strict';

    var core = angular
        .module('app.core')
        .directive('redactor', ['$timeout', function($timeout) {
          return {
            restrict: 'A',
            require: 'ngModel',
            // scope: {graphset: '=graphsetObject'},
            scope: true,
            link: function(scope, element, attrs, ngModel) {
                scope.redactorLoaded = false;

                $timeout(function() {
                    $(element).addClass('redactor');
                    var editor = $(element).redactor({
                        buttons: ['bold', 'italic', 'underline', 'deleted', 'lists', 'link'],
                        lang: 'fr',
                        // focus: false,
                        // toolbarOverflow: true,
                        air: true,
                        placeholder: 'Écrivez ce que vous voulez et sélectionnez du texte pour le formater.'
                    });
                    ngModel.$render(editor);
                });

                ngModel.$render = function(editor) {
                    if(angular.isDefined(editor)) {
                        $timeout(function() {
                            element.redactor('code.set', ngModel.$viewValue || '');
                            scope.redactorLoaded = true;
                        });
                    }
                };
            }
          };
        }]);
})();
(function ($)
{
	$.Redactor.opts.langs['fr'] = {
    "format": "Format",
    "image": "Image",
    "file": "Fichier",
    "link": "Lien",
    "bold": "Gras",
    "italic": "Italique",
    "deleted": "Barré",
    "underline": "Souligné",
    "bold-abbr": "B",
    "italic-abbr": "I",
    "deleted-abbr": "S",
    "underline-abbr": "U",
    "lists": "Listes",
    "link-insert": "Insérer un lien",
    "link-edit": "Editer le lien",
    "link-in-new-tab": "Ouvrir le lien dans un nouvel onglet",
    "unlink": "Retirer le lien",
    "cancel": "Annuler",
    "close": "Fermer",
    "insert": "Insérer",
    "save": "Sauvegarder",
    "delete": "Supprimer",
    "text": "Texte",
    "edit": "Editer",
    "title": "Titre",
    "paragraph": "Texte normal",
    "quote": "Citation",
    "code": "Code",
    "heading1": "Titre 1",
    "heading2": "Titre 2",
    "heading3": "Titre 3",
    "heading4": "Titre 4",
    "heading5": "Titre 5",
    "heading6": "Titre 6",
    "filename": "Nom",
    "optional": "Optionnel",
    "unorderedlist": "Liste non-ordonnée",
    "orderedlist": "Liste ordonnée",
    "outdent": "Réduire le retrait",
    "indent": "Augmenter le retrait",
    "horizontalrule": "Ligne",
    "upload-label": "Déposez un fichier ici ou ",
    "accessibility-help-label": "Editeur de texte enrichi",
    "caption": "Caption",
    "bulletslist": "Bullets",
    "numberslist": "Numbers",
    "image-position": "Position",
    "none": "None",
    "left": "Left",
    "right": "Right",
    "center": "Center",

    "accessibility-help-label": "Editeur de texte riche"

};

})(jQuery);

/**
 ** assume we are using a vm.sort variable in parent scope
**/
(function () {
    'use strict';

    var core = angular
        .module('app.core')
        .directive('sortableColumns', ['$q', function() {
          return {
            restrict: 'A',
            scope: {
                sortParams: '='
            },
            link: function(scope, element, attrs) {
                var marks = $('[data] .sort-mark', element);
                marks.append('<i class="caret"></i>');
                var elem = $('[data="'+scope.sortParams.sort+'"]', element);
                elem.addClass("sorted");

                $('th[data]', element).click(function(ev) {
                    var elem = $(ev.currentTarget);
                    var chevron = scope.sortParams.order=='asc' ? 'up' : 'down';
                    console.log('sortable column', element, ev, elem);
                    if (scope.sortParams.sort != elem.attr('data')) {
                        $('.sorted', element).removeClass("sorted")
                        scope.sortParams = { sort: elem.attr('data'), order: 'asc' };

                        elem.addClass("sorted");
                        $('.sort-mark', elem).addClass('fa fa-chevron-up');
                    } else {
                        scope.sortParams = { sort: elem.attr('data'), order: scope.sortParams.order!='asc' ? 'asc' : 'desc' };
                        chevron = scope.sortParams.order=='asc' ? 'up' : 'down';
                        $('.sort-mark', elem).addClass('fa fa-chevron-'+chevron);
                    }
                    scope.$applyAsync();
                });
            }
        };
    }]);
})();

(function () {
  'use strict';

  angular
      .module('app.core')
      .factory('timeservice', timeservice);

  timeservice.$inject = ['$http', '$q', '$localStorage', 'exception', 'logger'];
  /*  */
  function timeservice() {
  	var service = {
  		formatDuration: formatDuration
  	};
  	return service;

  	function formatDuration(value) {
	    var duration = moment.duration(parseFloat(value), 's');
	    var label = duration.humanize();
	    var minutes = duration.as('minutes');
	    if (minutes < 720 && minutes >= 45) {
	      label = (duration.hours()>0 ? duration.humanize()+' ' : '')+ duration.minutes() + ' minutes';
	    }
	    return label;
  	}

  }
})();

(function () {
    'use strict';

    angular
        .module('app.core')
        .service('uploadApi', uploadApi);

    uploadApi.$inject = ['MainService', 'Upload'];
    /*  */
    function uploadApi(MainService, Upload) {
        var service = {
            upload: upload
        };
        return service;

        function upload(params, config) {
            var endpoint = params.endpoint;
            delete params.endpoint;
            return Upload.upload($.extend({
                url: MainService.commonApiEndpoint() + endpoint,
                data: params
            }, config || {}));
        };
    }
})();

String.prototype.ellipsis = function(length, suffix) {
	if (typeof(suffix)=='undefined') {
		suffix = '...';
	}
	if (this != undefined && this.length>length) {
		return this.substring(0, length) + suffix;
	}
	return this;
};

if (!Array.prototype.includes) {
	Array.prototype.includes = function(item) {
		return this.indexOf(item) > -1;
	};
}
(function () {
    'use strict';

    angular
        .module('blocks.analytics')
        .service('AnalyticsService', analytics);


    analytics.$inject = ['$q', '$rootScope', '$http', 'userservice', 'myservice', 'dataservice', 'exception', 'logger', '$localStorage', 'MainService', '$location', '$sce'];
    /*  */
    function analytics($q, $rootScope, $http, userservice, myservice, dataservice, exception, logger, $localStorage, MainService, $location, $sce) {
      const graphSets = {
        music: [
          {
            position: 0,
            slug: 'connection',
            id: 'connection',
            title: 'Connexions',
            url: 'https://lookerstudio.google.com/embed/reporting/65da4f51-3dc4-4005-a046-6f5af84add5a/page/35FEC?params=%7B%22df51%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df52%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds110.aug%22%3A%22{{station_stats_factor}}%22%7D',
            all_stations_url: 'https://lookerstudio.google.com/embed/reporting/65da4f51-3dc4-4005-a046-6f5af84add5a/page/35FEC',
            ga3_url: 'https://datastudio.google.com/embed/reporting/094d1df8-2a1a-46f2-9273-73ce3cbb1954/page/35FEC?params=%7B%22df51%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df52%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds1.dxf%22%3A%22{{station_stats_factor}}%22%7D',
            ga3_all_stations_url: 'https://datastudio.google.com/embed/reporting/094d1df8-2a1a-46f2-9273-73ce3cbb1954/page/35FEC',
            width: 1400,
            height: 2450
          },
          {
            position: 1,
            slug: 'activity',
            id: 'activity',
            title: 'Activité',
            url: 'https://lookerstudio.google.com/embed/reporting/65da4f51-3dc4-4005-a046-6f5af84add5a/page/gvzDC?params=%7B%22df51%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df52%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds110.aug%22%3A%22{{station_stats_factor}}%22%7D',
            all_stations_url: 'https://lookerstudio.google.com/embed/reporting/65da4f51-3dc4-4005-a046-6f5af84add5a/page/gvzDC',
            ga3_url: 'https://datastudio.google.com/embed/reporting/094d1df8-2a1a-46f2-9273-73ce3cbb1954/page/gvzDC?params=%7B%22df51%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df52%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds1.dxf%22%3A%22{{station_stats_factor}}%22%7D',
            ga3_all_stations_url: 'https://datastudio.google.com/embed/reporting/094d1df8-2a1a-46f2-9273-73ce3cbb1954/page/gvzDC',
            width: 1400,
            height: 1900
          },
          {
            position: 2,
            slug: 'audience',
            id: 'audience',
            title: 'Audience',
            url: 'https://lookerstudio.google.com/embed/reporting/65da4f51-3dc4-4005-a046-6f5af84add5a/page/W0lEC?params=%7B%22df51%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df52%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds110.aug%22%3A%22{{station_stats_factor}}%22%7D',
            all_stations_url: 'https://lookerstudio.google.com/embed/reporting/65da4f51-3dc4-4005-a046-6f5af84add5a/page/W0lEC',
            ga3_url: 'https://datastudio.google.com/embed/reporting/094d1df8-2a1a-46f2-9273-73ce3cbb1954/page/W0lEC?params=%7B%22df51%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df52%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds1.dxf%22%3A%22{{station_stats_factor}}%22%7D',
            ga3_all_stations_url: 'https://datastudio.google.com/embed/reporting/094d1df8-2a1a-46f2-9273-73ce3cbb1954/page/W0lEC',
            width: 1400,
            height: 2400
          },
        ],
        game: [
          {
            position: 0,
            slug: 'dashboard',
            id: 'dashboard',
            title: 'Résumé',
            url: 'https://lookerstudio.google.com/embed/reporting/3392471a-c383-4360-8a3b-0f3eb310b431/page/gvzDC?params=%7B%22df84%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df85%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds28.dxf%22%3A%22{{station_stats_factor}}%22%7D',
            all_stations_url: 'https://lookerstudio.google.com/embed/reporting/3392471a-c383-4360-8a3b-0f3eb310b431/page/gvzDC',
            ga3_url: 'https://datastudio.google.com/embed/reporting/c25b5860-1091-4ff1-9474-dd51f2a1dbbd/page/gvzDC?params=%7B%22df84%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22df85%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580{{station_id}}%22,%22ds112.dxf%22%3A%22{{station_stats_factor}}%22%7D',
            ga3_all_stations_url: 'https://datastudio.google.com/embed/reporting/c25b5860-1091-4ff1-9474-dd51f2a1dbbd/page/gvzDC',
            width: 1400,
            height: 2450
          },
        ]
      };
      var stationId;


      var link = {
        getServices: getServices,
        getGraphSets: getGraphSets,
        setStoredStation: setStoredStation
      };

      return link;


      function getServices(params) {
        return myservice.me() && myservice.me().station.creative_fields.filter(function(v) {return Object.keys(graphSets).includes(v);}) || [];
      }

      function getGraphSets(_service, _stationId, _stationFactor, _ga4) {
        if (!myservice.isAdmin() && !(myservice.isAuthed() && myservice.me().level=='manager' && myservice.me().station.id==_stationId)) return [];
        return graphSets[_service].reduce(function(acc, v) {
          var by_station_url = _ga4 ? v.url : v.ga3_url;
          var all_stations_url = _ga4 ? v.all_stations_url : v.ga3_all_stations_url;
          v.builtUrl = $sce.trustAsResourceUrl(_stationId ? by_station_url.replace(/{{station_id}}/g, _stationId).replace(/{{station_stats_factor}}/g, _stationFactor) : all_stations_url);
          acc.push(v);
          return acc;
        }, []);
      }


      function setStoredStation(station) {
        $localStorage.station = $localStorage.station || {};
        if (station) {
          $localStorage.station = station;
        }
        stationId = station;
        return $localStorage.station;
      }
    }
})();

(function () {
    'use strict';

    angular
        .module('app.dashboard')
        .directive('dashboardGraphset', [function() {
          return {
            scope: {
              graphSet: '=graphset'
            },
            replace: true,
            templateUrl: 'app/dashboard/analytics/graphset.html',
            controller: ['$scope', '$timeout', function($scope, $timeout) {
            }]
          };
        }]);
})();
(function () {
    'use strict';


    angular
        .module('app.dashboard')
        .controller('DashboardController', ['$timeout', 'logger', '$scope', '$rootScope', '$q', '$state', '$stateParams', '$localStorage', '$location', 'userservice', 'AnalyticsService', 'dataservice', 'myservice', 'moment', '$uibModal', '$filter', '$translate', DashboardController])

    function DashboardController($timeout, logger, $scope, $rootScope, $q, $state, $stateParams, $localStorage, $location, userservice, AnalyticsService, dataservice, myservice, moment, $uibModal, $filter, $translate) {
        var vm = this;
        this.myservice = myservice;
        this.graphSet = null;
        $scope.ga4 = true;

        var stationId = null;
        var stationStatsFactor = {
          music: null,
          game: null
        };


        $scope.me = myservice.me();
        console.log('dashboard me', $scope.me);
        if (myservice.isAuthed() && ['manager', 'admin'].includes(myservice.me() && myservice.me().level)) {
          if (stationId = (myservice.isAdmin() && $location.search().station || myservice.me().station.id)) {
            dataservice.getStation(stationId).then(function(station) {
              setStation(station);
            });
          }
        } else {
          console.log('warning', 'not authed');
          myservice.signOut();
          $localStorage.redirectAfterSignInPath = 'dashboard';
          $state.go('auth.signIn');
        }
  
        $translate('STATS').then(function(txt) {
          vm.title = txt;
        });
        $translate('STATS.SUBTITLE').then(function(txt) {
          vm.subtitle = txt;
        });

        this.services = AnalyticsService.getServices(); // internal services
        this.service = this.services.includes($location.search().service) && $location.search().service || this.services[0]; // @TODO set back to first service when they are available
        this.segments = [];
        this.profiles = [];
        // graphs
        this.graphs = [];
        this.graphSets = [];



        this.setService = function(service) {
          //
          vm.service = service;
          if ($stateParams.service != service) {
            $location.search('service', service ? service : 'music');
          }
          setStation($scope.station);
        };


        /** STATIONS **/
        this.stations = [];
        var filter = this.stationFilter = '';
        var page = 1;

        function filterStations(filter, old) {
          if (!myservice.isAdmin()) { return ; } // @TODO remove
          dataservice.getStations(filter, {params: {page: page || $scope.page}})
          .then(function (data) {
            vm.stations = data.items;

            stationId = parseInt($location.search().station) || null;
            if (stationId) {
              dataservice.getStation(stationId);
            }
          });
        }
        $scope.$watch(function() {return vm.stationFilter}, filterStations);
        this.setStation = setStation;


        // select tab
        $scope.selectTab = function($event, $index, _graphSet) {
          $scope.currentGraphSetPosition = _graphSet.position;
          vm.graphSet = _graphSet;
          if ($('body').length) {
            $('body').scrollTop(0);
          }
          $scope.$broadcast('selectiveLoadMedia');
        };

        this.toggleGA4 = toggleGA4;

        function toggleGA4() {
          console.log('$scope.ga4', $scope.ga4);
          $scope.ga4 = !$scope.ga4;
          console.log('$scope.ga4', $scope.ga4);
          $scope.graphSets = AnalyticsService.getGraphSets(vm.service, stationId, stationStatsFactor[vm.service], $scope.ga4);
          $scope.$broadcast('selectiveLoad');
        };


        function setStation(station) {
          stationId = station ? station.id : null;
          $location.search('station', station ? station.id : null);
          console.log('set station', station, 'isadmin', myservice.isAdmin(), $scope.ga4);

          if (!station && myservice.isAdmin()) {
            station = {name: $filter('translate')('STATS.ALL_STATIONS'), id: ''};
          } else {
            dataservice.station = station;
            stationStatsFactor.music = station.settings.stats_factor || 100;
            stationStatsFactor.game = station.settings.game_stats_factor || 100;
            $location.search('station', station ? station.id : null);
          }
          $scope.station = station;
          vm.stationFilter = '';
          $scope.graphSets = AnalyticsService.getGraphSets(vm.service, stationId, stationStatsFactor[vm.service], $scope.ga4);
          $scope.$broadcast('selectiveLoad');
        };
        return vm;
}
})();
(function() {
    'use strict';

    angular
        .module('app.dashboard')
        .run(appRun);

    appRun.$inject = ['routerHelper', '$rootScope', '$interval'];
    /*  */
    function appRun(routerHelper, $rootScope, $interval) {
        routerHelper.configureStates(getStates());

        function getStates() {
            return [
                {
                    state: 'dashboard',
                    config: {
                        url: '/analytics?active&level&viewApp&query&segment&service',//&station&startDate&endDate&startDate2&endDate2',
                        templateUrl: 'app/dashboard/dashboard.html',
                        controller: 'DashboardController',
                        controllerAs: 'vm',
                        title: 'Dashboard',
                        settings: {
                            nav: 2,
                            level: ['manager', 'admin'],
                            content: '&#9;<span class="smartphone-hidden">Statistiques</span>'
                        },
                        onExit: ['$rootScope', '$interval', function($rootScope, $interval) {
                            for (var p in $rootScope.realtime) {
                                $interval.cancel($rootScope.realtime[p]);
                                $rootScope.realtime[p] = null;
                            }
                        }]
                    }
                }
            ];
        }
    }
})();

(function () {
  'use strict';

  angular
    .module('app.dashboard')
    .directive('poll', function() {
      return {
        restrict: 'A',
        link: ['scope', 'element', 'attrs', function(scope, element, attrs) {
        }]
      };
    });
})();

(function () {
  'use strict';

  angular
    .module('blocks.analytics')
    .service('DataProcessingService', processData);

  processData.$inject = ['$q', 'myservice', 'exception', 'logger', '$localStorage'];

  /*  */
  function processData($q, myservice, exception, logger, $localStorage) {
    return {
      convert: convert
    };


    function convert(data, format, options) {
      options = options ? options : {};
      var exclude = ['doc_count'];
      switch (format) {
        // @TODO use a worker
        case 'csv':
          var output = '';
          // headers
          var headers = [];
          for (var p in data[0]) {
            if (Array.isArray(options.columns) && options.columns.length) {
              if (!options.columns.includes(p)) {
                continue;
              }
            }
            headers.push(p);
            if (Array.isArray(options.columns) && options.columns.length==1) { break; }
            output += p + ';';
          }
          if (output.length) { output += "\n"; }
          // data
          angular.forEach(data, function(item) {
            angular.forEach(headers, function(header) {
              var val = item[header];
              if (item[header] instanceof Object) {
                if (item[header].hasOwnProperty('name')) {
                  val = item[header].name;
                } else if (item[header].hasOwnProperty('value')) {
                  val = item[header].value;
                } else {
                  val = '';
                }
              }

              output += val + (headers.length>1 ? ';' : '');
            });
            output += "\n";
          });
          // console.log(data, format, options, headers, output);
          return output;
      }
    }
  }
})();

(function() {
    'use strict';

    angular
        .module('app.layout')
        .directive('htSidebar', htSidebar);

    /*  */
    function htSidebar () {
        // Opens and closes the sidebar menu.
        // Usage:
        //  <div ht-sidebar">
        //  <div ht-sidebar whenDoneAnimating="vm.sidebarReady()">
        // Creates:
        //  <div ht-sidebar class="sidebar">
        var directive = {
            link: link,
            restrict: 'EA',
            scope: {
                whenDoneAnimating: '&?'
            }
        };
        return directive;

        function link(scope, element, attrs) {
            var $sidebarInner = element.find('.sidebar-inner');
            var $dropdownElement = element.find('.sidebar-dropdown a');
            element.addClass('sidebar');
            $dropdownElement.click(dropdown);

            function dropdown(e) {
                var dropClass = 'dropy';
                e.preventDefault();
                if (!$dropdownElement.hasClass(dropClass)) {
                    $sidebarInner.slideDown(350, scope.whenDoneAnimating);
                    $dropdownElement.addClass(dropClass);
                } else if ($dropdownElement.hasClass(dropClass)) {
                    $dropdownElement.removeClass(dropClass);
                    $sidebarInner.slideUp(350, scope.whenDoneAnimating);
                }
            }
        }
    }
})();

(function() {
    'use strict';

    angular
        .module('app.layout')
        .directive('htTopNav', htTopNav);

    /*  */
    function htTopNav () {
        var directive = {
            bindToController: true,
            controller: 'TopNavController',
            controllerAs: 'vm',
            restrict: 'EA',
            scope: {
                'navline': '='
            },
            templateUrl: 'app/layout/ht-top-nav.html'
        };

        /*  */
        function TopNavController() {
            var vm = this;
        }

        return directive;
    }
})();

(function() {
    'use strict';

    angular
        .module('app.layout')
        .controller('ShellController', ShellController);

    ShellController.$inject = ['$rootScope', '$timeout', '$http', '$localStorage', 'myservice', 'config', 'logger'];
    /*  */
    function ShellController($rootScope, $timeout, $http, $localStorage, myservice, config, logger) {
        var vm = this;
        vm.busyMessage = 'Please wait ...';
        vm.isBusy = true;


        activate();

        function activate() {
            // logger.success(config.appTitle + ' loaded!', null);
        }
    }
})();

(function() {
    'use strict';

    angular
        .module('app.layout')
        .controller('SidebarController', SidebarController);

    SidebarController.$inject = ['$scope', '$state', '$window', 'routerHelper', 'myservice', 'MainService'];
    /*  */
    function SidebarController($scope, $state, $window, routerHelper, myservice, MainService) {
        var vm = this;
        var states = routerHelper.getStates();
        vm.isCurrent = isCurrent;
        vm.logout = logout;

        vm.myservice = myservice;

        var activated = false;
        $scope.$watch(function() {
            return myservice.isAuthed() && myservice.me();
        }, function(v, o) {
            if (v && v.level && !activated) {
                getNavRoutes(myservice.me());
            }
        });


        function logout() {
            console.log(myservice.isAuthed())
          if (myservice.isAuthed()) {
            myservice.signOut();
            $window.location.href = MainService.accountsBaseUrl() + 'users/sign_out?redirect_uri=' + $state.href('auth.signIn', {}, {absolute: true});
          }
          return false;
        }

        function getNavRoutes(me) {
            activated = true;
            vm.navRoutes = states.filter(function(r) {
                return r.settings && r.settings.nav && r.settings.level.includes(me.level);
            }).sort(function(r1, r2) {
                return r1.settings.nav - r2.settings.nav;
            });
        }

        function isCurrent(route) {
            if (!route.title || !$state.current || !$state.current.title) {
                return '';
            }
            var menuName = route.title;
            return $state.current.title.substr(0, menuName.length) === menuName ? 'current' : '';
        }
    }
})();

(function () {
    'use strict';

    /* 
     * use this directive to open a modal detailing the given service and proposing to be contacted
     * by commercial service to activate it
     *
     * The directive needs only one attribute to work : 
     * - the service object, given as directive attribute. 
     * You can add a third argument parent-selector that will be used to select the element to apply the modal to as follow :
     * > angular.element($document[0].querySelector(parentSelector))
     *
     * Example for a transfer method and a user object, both accessible from the current scope
     *    <div class="my-button" service-activation-request="{slug: 'music', name: '1D touch Musique', description: 'blablabla'}" choice-arguments="user"></div>
     *
     */

    var core = angular
        .module('app.services')
        .directive('serviceActivationRequest', ['$q', '$stateParams', 'logger', '$uibModal', 'myservice', 'userservice', function($q, $stateParams, logger, $uibModal, myservice, userservice) {
          return {
            restrict: 'A',
            scope: {
                service: '=serviceActivationRequest',
                typeOfRequest: '@typeOfServiceRequest',
                parentSelector: '=parentSelector'
            },
            link: function(scope, element, attrs) {
                $(element).click(function() {
                    var parentElem = scope.parentSelector ? 
                        angular.element($document[0].querySelector(parentSelector)) : undefined;
                    var $uibModalInstance = $uibModal.open({
                      animation: true,
                      ariaLabelledBy: 'modal-title',
                      ariaDescribedBy: 'modal-body',
                      templateUrl: 'app/services/_service_activation_request.html',
                      controller: ['$scope', function($scope) {
                        var $ctrl = this;
                        $ctrl.query = '';
                        $ctrl.me = myservice.me(); 
                        $ctrl.service = scope.service;
                        $ctrl.type = scope.typeOfRequest;
                        $ctrl.message = scope.typeOfRequest == 'service_activation' ? '' : 'Je souhaite un rapport des statistiques pour la période du xx/xx/xx au xx/xx/xx';
                        $ctrl.email = $ctrl.me.email || null; 
                        $ctrl.phone = null; 
                        console.log('request form', scope, $ctrl);

                        $ctrl.ok = function () {
                          var args = {
                            service: 'commercial',
                            type: scope.typeOfRequest,
                            contact: {email: $ctrl.email, phone: $ctrl.phone},
                            data: scope.service,
                            message: scope.message
                          }
                          $uibModalInstance.close(args);
                        };
                        $ctrl.cancel = function () {
                          $uibModalInstance.dismiss('cancel');
                        };
                      }],
                      controllerAs: '$ctrl',
                      resolve: {
                        service: function() {
                          return scope.service;
                        },
                        typeOfRequest: function() {
                          return scope.typeOfRequest;
                        }
                      },
                      size: 'md',
                      appendTo: parentElem
                    });
                    $uibModalInstance.result.then(function(data) {
                      userservice.contactRequest(data).then(function(response) {
                        logger.success("Vous serez très prochainement recontacté par nos équipes commerciales. Merci.")
                      }, function(response) {
                        console.log(response)
                        logger.error("Une erreur est survenue. Veuillez ré-essayer ultérieurement")
                      });
                    }, function(reason) {
                        console.log('dismiss', reason);
                    });
                });
            }
        };
    }]);
})();
(function () {
    "use strict";

    angular
        .module('app.services')
        .controller('ServicesController', ServicesController);

    ServicesController.$inject = ['$scope', '$state', '$location', 'dataservice', 'myservice', 'logger', 'MainService', 'AnalyticsService', '$translate', 'userservice'];
    /*  */
    function ServicesController($scope, $state, $location, dataservice, myservice, logger, MainService, AnalyticsService, $translate, userservice) {
      var vm = this;
      var loading = false;
      vm.rgpd_url = MainService.accountsBaseUrl() + 'rgpd';

      $translate('SERVICES').then(function(txt) {
        vm.title = txt;
      });
      $translate('SERVICES.SUBTITLE').then(function(txt) {
        vm.subtitle = txt;
      });


      var stationId = null;
     
      if (myservice.isAuthed()) {
        $scope.me = myservice.me();
        console.log('authed', $scope.me);
        if (stationId = ($location.search().station || $scope.me.station.id)) {
          dataservice.getStation(stationId).then(function(station) {
            $scope.station = station;
            updateServicesState($scope.station)
          });
        }
      } else {
        $state.go('auth.signIn');
      }

      $scope.showConfirm = function(flag) {
        $('#confirmation-dialog .modal').modal(flag ? 'show':'hide');
      };
      $scope.reinvite = function(user, service) {
        userservice.reinvite(user, service).then(function(response) {
          logger.success('OK :)', response);
        }, function(reason) {
          logger.error("Une erreur s'est produite :(", reason);
        });
      };

      $scope.askForStatsReport = function(service, $event) {
        if (!service.remote_service) return;
        location.href = 'mailto:diffusion@divercities.eu?subject=Demande de statistiques '+service.name +'&body=Bonjour,\nje souhaite consulter les statistiques '+service.name+' du xx/xx/xx au xx/xx/xx.\nNom: xxxxx\nPrénom: xxxxx';
        $event.stopPropagation();
        $event.preventDefault();
        return false;
      };
      $scope.statsSref = function(service) {
        return service.remote_service ? 'services()' : 'dashboard({service: service.slug})';
      };

      async function updateServicesState(station) {
        vm.services = await MainService.fetchApps();

        var currentLanguage = $translate.use();
        
        for (var idx = 0; idx < vm.services.length; idx++) {
          vm.services[idx].enabled = station.creative_fields.includes(vm.services[idx].slug);
          vm.services[idx].analyzable = !vm.services[idx].remote_service;
          vm.services[idx].name = vm.services[idx].name[currentLanguage] || vm.services[idx].name['fr'] || vm.services[idx].name;
          if (vm.services[idx].description) {
            vm.services[idx].description = vm.services[idx].description[currentLanguage] || vm.services[idx].description['fr'] || vm.services[idx].description;
          }
        }
      }
    }
})();

(function() {
    'use strict';

    angular
        .module('app.services')
        .run(appRun);

    appRun.$inject = ['routerHelper'];
    /*  */
    function appRun(routerHelper) {
        routerHelper.configureStates(getStates());

        function getStates() {
            return [
                {
                    state: 'services',
                    config: {
                        url: '/',
                        templateUrl: 'app/services/services.html',
                        controller: 'ServicesController',
                        controllerAs: 'vm',
                        title: 'Vos ressources',
                        settings: {
                            nav: 1,
                            level: ['manager', 'admin'],
                            content: '&#9;<span class="smartphone-hidden">Vos ressources</span>'
                        }
                    }
                }
            ];
        }
    }
})();

(function () {
    'use strict';

    angular
        .module('app.auth')
        .service('SignInApi', signInApi);

    signInApi.$inject = ['$resource', 'MainService'];
    /*  */
    function signInApi($resource, MainService) {
        return $resource(MainService.commonApiEndpoint() + 'oauth/:action', {action: 'token'}, {
            getToken: {method: 'POST', params: {action: 'token'}, isArray: false},
            revokeToken: {method: 'POST', params: {action: 'revoke'}, isArray: false}
        });
    }
    function signUpApi($resource) {
        return $resource(MainService.accountsBaseUrl() + 'users/auth/:provider', {provider: 'mra'});
    }

})();

(function () {
    'use strict';

    angular
        .module('app.auth')
        .controller('SignInController', SignInController);

    SignInController.$inject = ['$rootScope', '$scope', '$localStorage', '$state', 'myservice'];
    function SignInController($rootScope, $scope, $localStorage, $state, myservice) {
        var vm = this;
        $scope.$parent.classes = 'sign-in';

        var redirectAfterSignInPath = $localStorage.redirectAfterSignInPath;
        delete $localStorage.redirectAfterSignInPath;
        if (myservice.isAuthed() && redirectAfterSignInPath) {
            console.log('sign-in go to', redirectAfterSignInPath);
            $state.go(redirectAfterSignInPath);
        }
    }

    function bgImage(nbImages) {
        var NB_IMAGES = 8;
        nbImages = nbImages || NB_IMAGES;
        return 'bg-' + Math.ceil(Math.random() * NB_IMAGES);
    }
})();

(function() {
    'use strict';

    angular
        .module('app.auth')
        .config(config)
        .run(appRun);


    /*  */
    config.$inject = ['$httpProvider'];
    function config($httpProvider) {
        $httpProvider.defaults.useXDomain = true;
        delete $httpProvider.defaults.headers.common['X-Requested-With'];
    }

    appRun.$inject = ['$rootScope', '$q', 'routerHelper', 'session', '$state', '$localStorage', 'myservice', 'SignInApi', 'MainService'];
    /*  */
    function appRun($rootScope, $q, routerHelper, session, $state, $localStorage, myservice, SignInApi, MainService) {
        var otherwise = '/users/sign_in';
        routerHelper.configureStates(getStates(), otherwise);

        function getStates() {
          return [
            {
              state: 'auth',
              config: {
                abstract: true,
                url: '/users',
                settings: {
                },
                resolve: {
                  isAuthed: function() { return myservice.isAuthed(); }
                },
                views: {
                  main: {
                    // template: '<div ng-class="classes" ui-view="auth"/>',
                    templateUrl: 'app/sign_in/auth.html',
                    controller: ['$scope', '$rootScope', '$localStorage', function($scope, $rootScope, $localStorage) {
                      var vm = this;

                      $rootScope.bodyClasses = 'auth ' + bgImage();
                      $scope.hide = myservice.isAuthed();
                    }]
                  }
                },
                onExit: ['$rootScope', function($rootScope) {
                    $rootScope.bodyClasses = '';
                }]
              }
            },
            {
              state: 'auth.signIn',
              config: {
                url: '/sign_in',
                onEnter: ['$rootScope', '$state', '$stateParams', '$window', '$http', '$localStorage', '$location', '$timeout', 'SignInApi', function($rootScope, $state, $stateParams, $window, $http, $localStorage, $location, $timeout, SignInApi) {
                  $rootScope.hideSignin = false;
                  var givenToken;
                  if ($location.hash()) {
                    givenToken = $location.hash().split('&');
                    givenToken = givenToken.reduce(function(acc, v) {
                      var parts = v.split('=');
                      return (parts[0]==='access_token') ? acc.concat(parts[1]) : acc;
                    }, []);
                  }
                  if (givenToken) {
                    $rootScope.hideSignin = true;
                    var token = givenToken.pop();
                    angular.extend($localStorage, {user: {token: token}});
                    var redirectAfterSignInPath = $localStorage.redirectAfterSignInPath || 'services';
                    delete $localStorage.redirectAfterSignInPath;
                    myservice.getMe().then(function(data) {
                      // $localStorage.setItem('me', data);
                      ga('config', 'UA-61995285-11', {custom_map: {dimension1: 'user_level', dimension2: 'user_station_id', dimension3: 'user_creative_fields', dimension4: 'user_id', dimension5: 'user_station_name'}, user_id: data.id, transport_type: 'beacon'});
                      $timeout(function(){
                        console.log('redirect from signin to', redirectAfterSignInPath);
                        $state.go(redirectAfterSignInPath);
                      }, 1);
                    }, function(reject) {
                      $rootScope.$broadcast('unauthorized', $state.current);
                    });
                  } else {
                    $http.get('/api/config.json').then(function(content) {
                      var config = angular.fromJson(content.data);
                      $window.location.href = MainService.accountsBaseUrl() + 'oauth/authorize?response_type=token&client_id=' + config.APPLICATION_ID + '&scope=manager+stats&redirect_uri=' + $state.href('auth.signIn', {}, {absolute: true});
                    });
                  }
                }],
                views: {
                  auth: {
                    // templateUrl: 'sign_in.html',
                    controller: 'SignInController',
                    controllerAs: 'signInCtrl'
                  }
                }
              }
            },
            {
              state: 'auth.denied',
              config: {
                url: '/denied',
                views: {
                  header: {
                    templateUrl: 'app/sign_in/partials/_denied_header.html',
                    controller: function() {}
                  },
                  footer: {
                    templateUrl: 'app/sign_in/partials/_worlds_footer.html',
                    controller: function() {
                    }
                  },
                  auth: {
                    templateUrl: 'app/sign_in/denied.html',
                    controller: ['$scope', '$stateParams', '$translate', '$localStorage', function($scope, $stateParams, $translate, $localStorage) {
                      var vm = this;
                      $scope.$state = $state;

                      $scope.$parent.classes = 'sign-in denied';

                      var translateString = 'ADMIN_APP';
                      $translate(translateString).then(function(result) {
                        $translate('UNAUTHORIZED_MSG', {app: result}).then(function(msg) {
                          $scope.UNAUTHORIZED_MSG = msg;
                        });
                      });

                      $localStorage.user = {};
                    }]
                  }
                }
              }
            },
            {
              state: 'auth.signOut',
              config: {
                url: '/sign_out',
                onEnter: ['$localStorage', '$rootScope', '$window', '$state', 'isAuthed', function($localStorage, $rootScope, $window, $state, isAuthed) {
                  if (myservice.isAuthed()) {
                    myservice.signOut();
                  }
                  console.log('signout is authed', myservice.isAuthed(), isAuthed);
                }],
                views: {
                  header: {
                    templateUrl: 'app/sign_in/partials/_signout_header.html',
                    controller: ['$scope', '$state', 'isAuthed', function($scope, $state, isAuthed) {
                      $scope.isAuthed = isAuthed;
                    }],
                    controllerAs: 'signOutHeaderCtrl'
                  },
                  footer: {
                    templateUrl: 'app/sign_in/partials/_worlds_footer.html',
                    controller: function() {
                    },
                    controllerAs: 'signOutFooterCtrl'
                  },
                  auth: {
                    templateUrl: 'app/sign_in/sign_out.html',
                    controller: ['$scope', '$localStorage', '$rootScope', '$state', '$stateParams', '$q', '$window', 'isAuthed', function($scope, $localStorage, $rootScope, $state, $stateParams, $q, $window, isAuthed) {
                      var vm = this;

                      $scope.$parent.classes = 'sign-in sign-out';
                      $scope.isAuthed = isAuthed;
                    }],
                    controllerAs: 'signOutCtrl'
                  }
                }
              }
            },
            {
              state: 'auth.signUp',
              config: {
                url: '/auth/:provider',
                onEnter: ['$rootScope', '$state', '$stateParams', '$window', '$location', function($rootScope, $state, $stateParams, $window, $location) {
                  var urlParams = '';
                  if ($location.search()) {
                    for (var p in $location.search()) {
                      urlParams = p+'='+$location.search()[p];
                    };
                  }
                  $window.location.href = MainService.accountsBaseUrl() + 'users/auth/' + $stateParams.provider + '?dest=/oauth/authorize&redirect_uri=' + $state.href('auth.signIn', {}, {absolute: true}) + urlParams;
                }],
                views: {
                  auth: {
                    templateUrl: 'sign_up.html',
                    controller: ['$scope', '$stateParams', function($scope, $stateParams) {
                      var vm = this;
                      $scope.$parent.classes = 'sign-up';
                      $scope.$parent.provider = $stateParams.provider;

                      vm.signUp = function() {
                        var params = {
                          grant_type: 'password', // jshint ignore:line
                          username: vm.email,
                          password: vm.password
                        };

                        $('.new_user .submit-field input').addClass('loading');
                        var animationEnd = 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend';
                      };
                    }],
                    controllerAs: 'signUpCtrl'
                  },
                  header: {
                    templateUrl: 'app/sign_in/partials/_signup_mra_header.html',
                    controller: function() {}
                  },
                  footer: {
                    template: '<div class="logo-bottom"><img src="/images/client_logos/logo_RRA.png" alt="Logo rra"></div>',
                    controller: function() {}
                  }
                }
              }
            },
            {
              state: 'auth.signUpPassword',
              config: {
                url: '/auth/:provider/forgotten',
                views: {
                  auth: {
                    templateUrl: 'partials/_signup_mra_forgotpassword.html',
                    controller: ['$scope', '$stateParams', function($scope, $stateParams) {
                      var vm = this;
                      $scope.$parent.classes = 'sign-up forgot-password';
                      $scope.$parent.provider = $stateParams.provider;

                      vm.signUp = function() {
                        var params = {
                          grant_type: 'password', // jshint ignore:line
                          username: vm.email,
                          password: vm.password
                        };

                        $('.new_user .submit-field input').addClass('loading');
                        var animationEnd = 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend';
                      };
                    }],
                    controllerAs: 'signUpCtrl'
                  },
                  header: {
                    templateUrl: 'app/sign_in/partials/_signup_mra_forgotpassword_header.html',
                    controller: function() {}
                  },
                  footer: {
                    template: '<div class="logo-bottom"><img src="/images/client_logos/logo_RRA.png" alt="Logo rra"></div>',
                    controller: function() {}
                  }
                }
              }
            }
          ];
        }
    }

    function bgImage(nbImages) {
      var NB_IMAGES = 8;
      nbImages = nbImages || NB_IMAGES;
      return 'bg-' + Math.ceil(Math.random() * NB_IMAGES);
    }
})();

(function () {
    'use strict';

    var core = angular.module('app.auth');


    core.factory('interceptor', interceptor)
        .config(configure);



    configure.$inject = ['$httpProvider'];
    /*  */
    function configure($httpProvider) {
        $httpProvider.defaults.useXDomain = true;
        delete $httpProvider.defaults.headers.common['X-Requested-With'];
        
        $httpProvider.interceptors.push('interceptor');
    }


    interceptor.$inject = ['$window', '$q', '$localStorage', 'session', '$rootScope', 'logger', '$stateParams', '$injector'];
    function interceptor($window, $q, $localStorage, session, $rootScope, logger, $stateParams, $injector) {
        return {
          request: function(request) {
            // inject bearer
            if (request.skipAuthorization) {
              return request;
            }
            if (session.isAuthenticated()) {
              var token = $localStorage.user.token;

              if (session.config.authHeader && session.config.authToken) {
                token = session.config.authToken + ' ' + token;
              }

              request.headers[session.config.authHeader] = token;
            } else {
              if (API_KEY) request.headers['X-Authorization'] = 'Token token=' + API_KEY;
            }


            return request;
          },

          requestError: function(request) {
            throw { message: 'request error', request: request, data: {description: 'Request error'} };
          },

          response: function(response)  {
            if (response.data && !!response.data.meta) {
                response.data.$total = response.data.meta.total || 0;
            }

            // check bearer validity
            if (response.status === 401) {
                if (!response.data.code) {
                    // delete $localStorage.user;
                    // delete $localStorage.me;
                }
                console.log('interceptor status 401');
                $rootScope.$broadcast('denied', {error: 'response', status: 401, response: response});
                return $q.reject({status: response.status, data: {description: 'Unauthorized'}});
            }

            return response || $q.when(response);
          },

          responseError: function(response) {
            var $translate = $injector.get('$translate');
            var reason = {message: 'Could not fulfill request', data: {description: 'Erreur réseau'}};
            if (response.status === 404) {
              reason = {status: response.status, data: {description: 'Une ressource est introuvable... :('}};
            }
            // 422
            if (response.status === 422) {
              $translate('PROCESSING_ERROR').then(function(error) {
                reason = {status: response.status, data: {description: response.data.message}}
              })
              if (response.data && response.data.message)
                reason.data.description = response.data.message;
            }
            // 429
            if (response.status === 429) {
              return $q.reject({status: response.status, code: response.data.code, message: response.data.message, data: {description: response.data.message}});
            }

            // 401
            if (response.status === 401) {
              var canceller = $q.defer();
              var $state = $injector.get('$state');
              reason = {status: response.status, data: {description: 'Unauthorized... :/'}};

              console.log('interceptor', $state, $stateParams, response);
              if (!response.data.code) {
                var authPaths = [$state.get('auth.signOut'), $state.get('auth.denied'), $state.get('auth.signUp')];
                authPaths = authPaths.reduce(function(acc, v) {
                  return v ? acc.concat($state.get(v.name.split('.')[0]).url + v.url) : acc;
                }, []);
                if (!$state.includes('auth') && !$state.is('') && $state.current.name!=='' && !authPaths.includes($injector.get('$location').path()) && !($stateParams.code)) {
                  // $state.go('auth.signIn');
                  $rootScope.$broadcast('unauthorized', $state.current);
                }
              } else {
                if (!!response && !!response.data && response.data.code) {
                  switch (response.data.code) {
                    case 'restricted_access':
                      $localStorage.user = {};
                      // $state.go('auth.denied');
                      $rootScope.$broadcast('denied', {error: 'response-error', status: 401, response: response});
                      break;
                  }
                }
              }

              return response || $q.reject(reason);
            }
            if (response.status === -1) {
              logger.error('Erreur réseau :(', reason);
            }
            console.log('Erreur interceptor', response);
            return $q.reject(reason);
          }
        };
    }


})();

(function () {
    'use strict';

    /* 
     * use this directive to open a modal listing the stations with an instant search filter.
     *
     * The directive needs at least two attributes to work : 
     * - a callback method, given as directive attribute. 
     *   This is the method that will be called after the station has been chosen. 
     *   The callback method should have 2 arguments : 
     *   - args : some arguments you define with the 2nd directive attribute 
     *   - station : the station that has been chosen
     * - choice-arguments : an object you want to pass to the callback method when called
     * You can add a third argument parent-selector that will be used to select the element to apply the modal to as follow :
     * > angular.element($document[0].querySelector('.modal-stations ' + parentSelector))
     *
     * Example for a transfer method and a user object, both accessible from the current scope
     *    <div class="my-button" station-choose="transfer" choice-arguments="user"></div>
     *
     */

    var core = angular
        .module('app.stations')
        .directive('stationChoose', ['$q', '$stateParams', 'dataservice', 'logger', '$uibModal', 'myservice', 'userservice', function($q, $stateParams, dataservice, logger, $uibModal, myservice, userservice) {
          return {
            restrict: 'A',
            scope: {
                callbackMethod: '=stationChoose',
                parentSelector: '=parentSelector',
                choiceArguments: '=choiceArguments'
            },
            link: function(scope, element, attrs) {
                $(element).click(function() {
                    var deferred = $q.defer();
                    console.log('user-invite-one', scope.station);

                    console.log('choose', scope.choiceArguments);
                    var parentElem = scope.parentSelector ? 
                        angular.element($document[0].querySelector('.modal-stations ' + parentSelector)) : undefined;
                    var $uibModalInstance = $uibModal.open({
                      animation: true,
                      ariaLabelledBy: 'modal-title',
                      ariaDescribedBy: 'modal-body',
                      templateUrl: 'app/stations/_stations_choose.html',
                      controller: ['$scope', function($scope) {
                        var $ctrl = this;
                        $ctrl.query = '';

                        $ctrl.ok = function (chosen_station) {
                          if (chosen_station) {
                            console.log('choose', chosen_station);
                            $uibModalInstance.close(chosen_station);
                          }
                        };
                        $ctrl.cancel = function () {
                          $uibModalInstance.dismiss('cancel');
                        };
                        $ctrl.nextPage = function () {
                        };
                        $scope.$watch('$ctrl.query', function (v, o) {
                          var params = {params: {}};
                          dataservice.getStations(v, params).then(function(stations) {
                            $ctrl.stations = stations.items;
                          });
                        });
                      }],
                      controllerAs: '$ctrl',
                      resolve: {
                        station: function() {
                          return scope.choiceArguments;
                        }
                      },
                      size: 'md',
                      appendTo: parentElem
                    });
                    $uibModalInstance.result.then(function(chosen_station) {
                      scope.callbackMethod(scope.choiceArguments, chosen_station)
                    }, function(reason) {
                        console.log('dismiss', reason);
                    });
                });
            }
        };
    }]);
})();
(function () {
  'use strict';

  var core = angular
    .module('app.stations')
    .directive('codesForm', ['$q', '$localStorage', '$stateParams', 'dataservice', 'logger', '$uibModal', 'myservice', 'MainService', 'DataProcessingService', 
      function($q, $localStorage, $stateParams, dataservice, logger, $uibModal, myservice, MainService, DataProcessingService) {
      return {
        restrict: 'A',
        scope: {
          station: '='
        },
        link: function(scope, element, attrs) {
          $(element).click(function() {
            var deferred = $q.defer();

            var parentElem = scope.parentSelector ? 
              angular.element($document[0].querySelector('.modal-codes ' + scope.parentSelector)) : undefined;
            var $uibModalInstance = $uibModal.open({
              animation: true,
              ariaLabelledBy: 'modal-title',
              ariaDescribedBy: 'modal-body',
              templateUrl: 'app/stations/_station_codes.html',
              controller: ['$scope', 'station', 'codesCount', 'usedCodesCount', function($scope, station, codesCount, usedCodesCount) {
                var $ctrl = this;
                if (station) {
                  $ctrl.station = station;
                } else {
                  $ctrl.station = {};
                }

                $ctrl.myservice = myservice;
                $ctrl.apps = MainService.appsSlug();
                $ctrl.page = 0;
                $ctrl.fetching = false;
                $ctrl.disabled = false;
                $ctrl.filter_qry = '';
                $scope.$watch('$ctrl.filter_qry', function(val, old_val) {
                  if (val != old_val) {
                    $ctrl.filter({query: val});
                  }
                });
                
                $ctrl.codes = [];
                $ctrl.totalCodes = codesCount;
                $ctrl.usedCodes = usedCodesCount;
                $ctrl.inSerial = false;
                $ctrl.serialName = null;
                $ctrl.apps = myservice.isAdmin() ? MainService.appsSlug() : station && station.creative_fields;
                $ctrl.creative_fields = {};
                $ctrl.features = {creative_fields: {}};
                MainService.appsSlug().forEach(function(creative_field, index) {
                  $ctrl.creative_fields[creative_field] = false;
                  $ctrl.features.creative_fields[creative_field] = {};
                });
                
                $ctrl.params = { };

                $ctrl.ok = function() {
                  for (var creative_field in $ctrl.creative_fields) {
                    if (!$ctrl.creative_fields[creative_field]) {
                      delete $ctrl.features.creative_fields[creative_field];
                    } else {
                      if ($ctrl.features.creative_fields[creative_field] == {})
                        $ctrl.features.creative_fields[creative_field] = true;
                    };
                  }

                  if ($ctrl.features.creative_fields.length == 0)
                    delete $ctrl.features.creative_fields;


                  if ($ctrl.nbCodes>0) {
                    $uibModalInstance.close({nbCodes: $ctrl.nbCodes, station: $ctrl.station, features: $ctrl.features, serialName: $ctrl.serialName});
                  } else {
                    logger.error('Vous devez générer au moins un code !');
                  }
                };
                $ctrl.getSerials = function(params) {
                  var blobUrl;
                  dataservice.getCodesSerials(station, $.extend(params, {details: true}))
                  .then(function(data) {
                    // convert to CSV and save
                    $ctrl.serials = data;
                  });
                };
                $ctrl.filter = function(params) {
                  reinitPagination();
                  $ctrl.params = $.extend($ctrl.params, params);
                  if (!params.query) { $ctrl.filter_qry = ''; }
                  $ctrl.nextPage();
                }
                $ctrl.nextPage = function() {
                  if ($ctrl.meta && $ctrl.codes.length >= $ctrl.meta.total || $ctrl.fetching) { var deferred = $q.defer(); deferred.reject(); return deferred.promise.catch(angular.noop); }
                  $ctrl.page++;
                  $ctrl.fetching = true;
                  return $ctrl.getAll($.extend({page: $ctrl.page, per_page: 20}, $ctrl.params));
                }
                $ctrl.getAll = function(params) {
                  dataservice.getCodes(station, params)
                  .then(function(data) {
                    $ctrl.codes = $ctrl.codes.concat(data.items);
                    $ctrl.meta = data.meta;
                    $ctrl.fetching = false;
                  });
                };
                $ctrl.download_current = function() {
                  $ctrl.download($ctrl.params);
                }
                $ctrl.download = function(params, serialname) {
                  var filename = serialname ? serialname + '.csv' : 'code.csv';
                  dataservice.getCodes(station, $.extend({download: true}, params))
                  .then(function(data) {
                    to_csv(data.items, filename);
                  });
                };

                $ctrl.cancel = function() {
                  $uibModalInstance.dismiss('cancel');
                };

                var reinitPagination = function() {
                  $ctrl.codes = [];
                  $ctrl.page = 0;
                  $ctrl.meta = null;
                };
                var to_csv = function(items, filename) {
                  var blobUrl;
                  if (!filename) filename = 'codes.csv';
                  // convert to CSV and save
                  var csv = DataProcessingService.convert(items, 'csv');
                  var opts = {encoding: 'UTF-8', type: 'text/csv; charset=UTF-8'};
                  var blob = new Blob([csv], opts);
                  if (blobUrl !== null) {
                    window.URL.revokeObjectURL(blobUrl);
                  }
                  blobUrl = window.URL.createObjectURL(blob);

                  var a = $("<a style='display: none;'/>");
                  a.attr('href', blobUrl);
                  a.attr('download', filename);
                  $('.modal-codes .codes').append(a);
                  a[0].click();
                  a.remove();
                  // returns a URL you can use as a href
                  // $window.location.href = blobUrl;

                }
              }],
              controllerAs: '$ctrl',
              resolve: {
                station: function() { return angular.copy(scope.station); },
                codesCount: function() {
                  var deferred = $q.defer();
                  dataservice.getNbCodes(scope.station).then(function(data) {
                    deferred.resolve(data);
                  });
                  return deferred.promise;
                },
                usedCodesCount: function() {
                  var deferred = $q.defer();
                  dataservice.getNbCodes(scope.station, {state: 'used'}).then(function(data) {
                    deferred.resolve(data);
                  });
                  return deferred.promise;
                },
                services: function(MainService) {
                  return MainService.fetchApps();
                }
              },
              size: 'lg',
              appendTo: parentElem
            });
            $uibModalInstance.result.then(function(result) {
              if (result.serialName) {
                dataservice.generateCodesSerial(result.nbCodes, result.station, result.features, result.serialName).then(function(response) {
                  logger.success('\nLa série ' + result.serialName + ' de ' + result.nbCodes + ' codes a bien été généré.');
                  // refresh();
                });
              } else {
                dataservice.generateCodes(result.nbCodes, result.station, result.features).then(function(response) {
                  logger.success('\nLes ' + result.nbCodes + ' codes d\'activation vont être généré.');
                  // refresh();
                });
              }
              console.log('generating '+result.nbCodes+' codes', result.station);
            }, function(reason) {
              console.log('dismiss', reason);
            });

            return deferred.promise;
          });
        }
      };
    }]);
})();
(function () {
  'use strict';

  var core = angular
    .module('app.stations')
    .directive('stationForm', ['$q', 'dataservice', 'logger', '$uibModal', 'myservice', '$translate', function($q, dataservice, logger, $uibModal, myservice, $translate) {
      return {
        restrict: 'A',
        scope: {
          station: '=',
          onsuccess: '=',
          onerror: '='
        },
        link: function(scope, element, attrs) {
          $(element).click(function() {
            var deferred = $q.defer();

            var $uibModalInstance = $uibModal.open({
              animation: true,
              ariaLabelledBy: 'modal-title',
              ariaDescribedBy: 'modal-body',
              templateUrl: 'app/stations/_station_form.html',
              controller: ['$scope', 'dataservice', function($scope, dataservice) {
                const { id, name, description, logo, address_street, address_city, address_postal_code, address_country, client_id, has_validity_due_date, validity_due_date, has_validity_duration, validity_duration, renew_path, allowed_to_generate_coupon_invitation_codes, allowed_to_use_coupon_invitation_codes, coupon_invitation_codes_limit, managers_can_handle_validity, creative_fields, creative_field_accesses, settings, plans } = scope.station;
                var station = {id, name, description, logo, address_street, address_city, address_postal_code, address_country, client_id, has_validity_due_date, validity_due_date, has_validity_duration, validity_duration, renew_path, allowed_to_generate_coupon_invitation_codes, allowed_to_use_coupon_invitation_codes, coupon_invitation_codes_limit, managers_can_handle_validity, creative_fields, creative_field_accesses, settings, plans};

                var $ctrl = this;
                $ctrl.myservice = myservice;
                if (station && station.id) {
                  $ctrl.station = station;
                  $ctrl.station.managers_can_handle_validity = station.managers_can_handle_validity || false;
                  $ctrl.station.has_validity_due_date = station.has_validity_due_date || false;
                  $ctrl.station.validity_due_date = station.validity_due_date ? new Date(station.validity_due_date) : null;
                  $ctrl.default_creative_fields = station.creative_fields.reduce(function(acc,v) { return acc.concat({label: v, enabled: ($.inArray(v, station.settings.default_creative_fields)>-1)}); }, []);
                  $ctrl.station.settings.force_all_creative_fields_access = station.settings.force_all_creative_fields_access || false;
                  $ctrl.station.settings.managers_can_soft_delete_users = station.settings.managers_can_soft_delete_users || false;
                  $ctrl.station.settings.send_managers_notifications = station.settings.send_managers_notifications || false;
                  $ctrl.station.settings.max_station_downloads = parseInt($ctrl.station.settings.max_station_downloads) || 0;
                  $ctrl.station.settings.max_user_downloads = parseInt($ctrl.station.settings.max_user_downloads) || 0;
                  $ctrl.station.settings.ui = $ctrl.station.settings.ui;
                  $ctrl.max_station_downloads_deal = (!!station.settings.max_station_downloads ? station.settings.max_station_downloads : '-');
                  $ctrl.station.settings.limits_period = parseInt($ctrl.station.settings.limits_period) || 0;
                  $ctrl.station.settings.stats_factor = parseFloat($ctrl.station.settings.stats_factor) || 100.0;
                  $ctrl.station.settings.game_stats_factor = parseFloat($ctrl.station.settings.game_stats_factor) || 100.0;
                  $ctrl.station.settings.check_portal_account_validity = $ctrl.station.settings.check_portal_account_validity || false;
                  $ctrl.opsis_subscription_ids = $ctrl.station.settings.opsis_subscription_ids && $ctrl.station.settings.opsis_subscription_ids.split(',').reduce(function(acc,v) { acc[v] = true; return acc; }, {})
                } else {
                  $ctrl.station = {};
                  $ctrl.station.settings = {};
                  $ctrl.station.sso_setting = {};
                  $ctrl.station.creative_fields = [];
                  $ctrl.station.creative_field_accessess = {};
                  $ctrl.station.plans = {};
                }
                console.log('form station', $ctrl);
                    
                $ctrl.ok = function() {
                  $ctrl.station.settings.default_creative_fields = []
                  if ($ctrl.default_creative_fields) {
                    $ctrl.default_creative_fields.forEach(function(el) {
                      if (el.enabled)
                        $ctrl.station.settings.default_creative_fields.push(el.label);
                    })
                  }
                  if ($scope.stationEditForm.$valid) {
                    $ctrl.station.description = $(document).find('.station-form .description .redactor').redactor('code.get');
                    if (station.creative_fields && station.creative_fields.length || station.sso_setting && station.sso_setting.provider == 'code') {
                      const wt = $(document).find('.station-form .welcome-text .redactor').redactor('code.get');
                      $ctrl.station.settings.welcome_text = wt.length ? wt[0] : '';
                    }
                    if (station.creative_fields && station.creative_fields.length || station.sso_setting && station.sso_setting.provider == 'code') {
                      const it = $(document).find('.station-form .invitation-text .redactor').redactor('code.get');
                      $ctrl.station.settings.invitation_text = it.length ? it[0] : '';
                    }
                    if (typeof($ctrl.station.logo) == 'string') {
                      delete $ctrl.station.logo;
                    }
                    $ctrl.station.creative_fields.forEach(function(el) {
                      if ($ctrl[el+'_subscription_ids']) {
                        var subscription_ids = [];
                        for (var plan_id in $ctrl[el+'_subscription_ids']) {
                          if ($ctrl[el+'_subscription_ids'][plan_id]) {
                            subscription_ids.push(plan_id);
                          }
                        }
                        if (subscription_ids && subscription_ids.length) {
                          $ctrl.station.settings[el+'_subscription_ids'] = subscription_ids.join(',');
                        }
                      }
                    });
                    console.log('save station', $ctrl.station, 'initial', station);
                    dataservice.saveStation($ctrl.station).then(
                      function(response) {
                        // console.log(response);
                        deferred.resolve(response);
                        $uibModalInstance.close($ctrl.station);
                      },
                      function(reason) {
                        logger.error('La station existe probablement.');
                        deferred.reject('API_ERROR');
                      });
                  } else {
                    console.log('form nok');
                  }
                };

                $ctrl.cancel = function() {
                  deferred.reject();
                  $uibModalInstance.dismiss('cancel');
                };

                
              }],
              controllerAs: '$ctrl',
              size: 'lg'
            });
            $uibModalInstance.result.then(function(station) {
              if (scope.onsuccess) scope.onsuccess.call();
            }, function(reason) {
              if (scope.onerror && reason=='API_ERROR') scope.onerror.call();
              console.log('dismiss',reason);
            });

            return deferred.promise;
          });
        }
      };
    }]);
})();
(function () {
    "use strict";

    angular
        .module('app.stations')
        .controller('StationsController', StationsController);

    StationsController.$inject = ['$scope', '$q', '$timeout', 'dataservice', 'myservice', 'logger', 'MainService', '$uibModal', '$document', '$window', '$translate', '$localStorage'];
    /*  */
    function StationsController($scope, $q, $timeout, dataservice, myservice, logger, MainService, $uibModal, $document, $window, $translate, $localStorage) {
        var vm = this;
        vm.loading = false;

        vm.news = {
            title: 'Kiosque 1D lab',
            description: 'Manage your community, browse your stats.'
        };
        $translate('STATIONS.DESCRIPTION').then(function(txt) {
          vm.news.description = txt;
        });
        vm.apps = MainService.apps();
        vm.stations = {items:[], meta: {}};

        $translate('STATIONS').then(function(txt) {
          vm.title = txt;
        });
        $translate('STATIONS.SUBTITLE').then(function(txt) {
          vm.subtitle = txt;
        });

        var activated = false;
        vm.page = 0;
        vm.previousLength = -1;
        vm.children = {};
        vm.fetchedSort = false;
        vm.disabled = false;
        vm.filter = '';
        vm.nextPage = nextPage;
        vm.sortParams = { sort: 'name', order: 'asc' };
        var params = {sort: 'name', order: 'asc'};
        $scope.$watch('vm.sortParams', function(v,o) {
          params.sort = v.sort;
          params.order = v.order;

          if (activated) {
            myservice.me().then(refresh);
          }
        });

        vm.SCHEME = SCHEME;
        vm.APP = APP;
        vm.DC_DOMAIN = DC_DOMAIN;


        $scope.myservice = myservice;
        $scope.dataservice = dataservice;

        // stations create form callbacks
        $scope.onsuccess = function() { $scope.refresh(); logger.success('OK :)'); };
        $scope.onerror = function() { $scope.refresh(); logger.error("La station n'a pas été créée :/"); };


        $scope.$watch(function() {return $localStorage.me;}, function(v, o) {
          if (v && v.level=='admin' && !activated) {
            activate();
          }
        });

        function search(ev) {
          if (ev && (ev.keyCode !== 13)) return;
          vm.page = 0;
          vm.disabled = false;
          vm.stations = {items:[], meta: {}};
          vm.filter = vm.searchString;
          nextPage(true);
        }
        $scope.search = search;

        function activate() {
          activated = true;
          refresh();
        }

        function getMessageCount() {
            return dataservice.getMessageCount().then(function (data) {
                vm.messageCount = data;
                return vm.messageCount;
            });
        }

        // vm.page+1, false, vm.filter
        function nextPage(force=false, scroll=false) {
          if (force) {
            console.log('abort current request', vm.fetching);
            vm.fetching = false;
            vm.loading = false;
            vm.disabled = false;
          }
          if (vm.fetching) {
            console.log('awaiting for previous request');
            return;
          }

          vm.loading = vm.fetching = true;
          var _page = vm.page +1;
          var _filter = vm.filter;
          vm.fetchedFilter = vm.filter;
          return dataservice
            .getStations(_filter, {params: $.extend({page: _page, level: 1}, params)})
            .then(function(data) {
              console.log('nextpage: data', _filter, _page, vm, data, scroll, force);
              if (!vm.fetching) { console.log('same page or filter has changed', _page, _filter, vm, data); return; }

              angular.forEach(data.items, function(item) {
                if (!vm.stations.items.find(function(st) {return st.id==item.id})) {
                  if (vm.fetching) vm.stations.items.push(item);
                }
                if (item.has_children) {
                  if (vm.fetching) childrenStations(item.id);
                }
              });
              vm.stations.meta = data.meta;
              vm.fetchedSort = vm.sortParams;
              vm.previousLength = data.items.length;
              vm.page = _page;
              vm.loading = false;
              vm.disabled = false;
              vm.fetching = false;
              console.log('nextpage: processed', vm);
            }, function(reason) {
              vm.loading = false;
              vm.fetching = false;
            });
        }

        function refresh() {
          console.log('refresh', vm.page, vm.loading);
          if (!myservice.isAuthed() || vm.loading || myservice.me().level!='admin') return;
          vm.disabled = false;
          vm.loading = true;
          vm.page = 0;
          vm.previousLength = -1;
          vm.stations.items = [];
          vm.stations.meta = {};
          return nextPage(true)
            .then(function() {
              $timeout(function() { $('body').scrollTop(0); }, 500);
            });
        }
        $scope.refresh = refresh;

        function childrenStations(station_id) {
          return dataservice.getStations(vm.search_all ? vm.filter : null, {params: $.extend({parent_id: station_id, per_page: 100}, params)}).then(function(data) {
            vm.children[station_id] = data;
            // vm.fetched = false;
          });
        };

        $scope.sso_setting = function(args, parentSelector) {
          // console.log('sso setting', args, parentSelector);
          var parentElem = parentSelector ? 
            angular.element($document[0].querySelector('.modal-sso-setting ' + parentSelector)) : undefined;
          var $uibModalInstance = $uibModal.open({
            animation: true,
            ariaLabelledBy: 'modal-title',
            ariaDescribedBy: 'modal-body',
            templateUrl: 'app/stations/_station_sso_setting.html',
            controller: ['$scope', 'station', 'providers', 'casProviders', 'inforProviders', 'decalogProviders', 'bmGrenobleProviders', 'pmbProviders', 'ipProviders', function($scope, station, providers, casProviders, inforProviders, decalogProviders, bmGrenobleProviders, pmbProviders, ipProviders) {
              var $ctrl = this;
              $ctrl.station = station ? station : {sso_setting: {}};
              $ctrl.station.sso_setting = $ctrl.station.sso_setting ? $ctrl.station.sso_setting : {provider: null};
              $ctrl.providers = providers ? providers : [];
              $ctrl.providers.unshift('');
              $ctrl.inforProviders = inforProviders ? inforProviders : [];
              $ctrl.decalogProviders = decalogProviders ? decalogProviders : [];
              $ctrl.casProviders = casProviders ? casProviders : [];
              $ctrl.bmGrenobleProviders = bmGrenobleProviders ? bmGrenobleProviders : [];
              $ctrl.pmbProviders = pmbProviders ? pmbProviders : [];
              $ctrl.ipProviders = ipProviders ? ipProviders : [];
              console.log('sso setting params', station, $ctrl);

              $ctrl.accountsBaseUrl = MainService.accountsBaseUrl;
              $ctrl.ok = function() {
                if ($scope.ssoSettingForm.$valid) {
                  $uibModalInstance.close($ctrl.station);
                } else {
                }
              };

              $ctrl.cancel = function() {
                $uibModalInstance.dismiss('cancel');
              };
            }],
            controllerAs: '$ctrl',
            resolve: {
              station: function() { return args; },
              providers: function() {
                var deferred = $q.defer();
                dataservice.getClientProviders().then(function(data) {
                  console.log('providers', data);
                  deferred.resolve(data.items);
                });
                return deferred.promise;
              },
              casProviders: function() {
                var deferred = $q.defer();
                dataservice.getClientProviders({strategy: 'CAS'}).then(function(data) {
                  console.log('CAS providers', data);
                  data.items.concat('gminvent');
                  deferred.resolve(data.items);
                });
                return deferred.promise;
              },
              inforProviders: function() {
                var deferred = $q.defer();
                dataservice.getClientProviders({strategy: 'Infor'}).then(function(data) {
                  console.log('infor providers', data);
                  deferred.resolve(data.items);
                });
                return deferred.promise;
              },
              decalogProviders: function() {
                var deferred = $q.defer();
                dataservice.getClientProviders({strategy: 'DecalogWebservice'}).then(function(data) {
                  console.log('decalog providers', data);
                  deferred.resolve(data.items);
                });
                return deferred.promise;
              },
              bmGrenobleProviders: function() {
                var deferred = $q.defer();
                dataservice.getClientProviders({strategy: 'BMGrenoble'}).then(function(data) {
                  console.log('bmgrenoble providers', data);
                  deferred.resolve(data.items);
                });
                return deferred.promise;
              },
              pmbProviders: function() {
                var deferred = $q.defer();
                dataservice.getClientProviders({strategy: 'PMB'}).then(function(data) {
                  console.log('PMB providers', data);
                  deferred.resolve(data.items);
                });
                return deferred.promise;
              },
              ipProviders: function() {
                var deferred = $q.defer();
                dataservice.getClientProviders({strategy: 'IP'}).then(function(data) {
                  console.log('IP providers', data);
                  deferred.resolve(data.items);
                });
                return deferred.promise;
              }
            },
            size: 'lg',
            closeByDocument: false,
            appendTo: parentElem
          });
          $uibModalInstance.result.then(function(station) {
            console.log('save sso setting', station);
            dataservice.saveSsoSetting(station.sso_setting, station).then(function(response) {
              logger.success('OK :)');
              // refresh();
            });
            console.log('saving sso setting', station.sso_setting, station);
          }, function(reason) {
            console.log('dismiss', reason);
          });
        };

        $scope.client = function(args, parentSelector, displayResult = true, required_app=null) {
          var client_deferred = $q.defer();

          // console.log('client', args, parentSelector);

          var parentElem = parentSelector ? 
            angular.element($document[0].querySelector('.modal-client ' + parentSelector)) : undefined;
          var $uibModalInstance = $uibModal.open({
            animation: true,
            ariaLabelledBy: 'modal-title',
            ariaDescribedBy: 'modal-body',
            templateUrl: 'app/stations/_station_client.html',
            controller: ['$scope', 'station', function($scope, station) {
              var $ctrl = this;
              $ctrl.required_client_property = required_app;
              $ctrl.station = station ? station : {client: {}};
              $ctrl.station.client = $ctrl.station.client ? $ctrl.station.client : {name: $ctrl.station.name, munki_provider_id: null, tenk_group_id: null};
              console.log('client params', station, $ctrl);
              console.log('required_app', $ctrl.required_client_property);

              $ctrl.ok = function() {
                // console.log('ok', $ctrl, $scope.clientForm);
                if ($scope.clientForm.$valid) {
                  $uibModalInstance.close($ctrl.station);
                } else {
                }
              };

              $ctrl.cancel = function() {
                $uibModalInstance.dismiss('cancel');
              };
            }],
            controllerAs: '$ctrl',
            resolve: {
              station: function() { 
                var deferred = $q.defer();
                if (args.id) {
                  dataservice.getStation(args.id).then(function(data) {
                    deferred.resolve(data);
                  });
                } else {
                  deferred.reject();
                }
                return deferred.promise;
              },
              
            },
            size: 'lg',
            appendTo: parentElem
          });
          $uibModalInstance.result.then(function(station) {
            console.log('save client', station);
            dataservice.saveClient(station.client, station).then(function(response) {
              if (displayResult) 
                logger.success('Client Updated :)');
              // refresh();
              if (station.client.munki_provider_id || station.client.tenk_group_id) {
                station = response;
                angular.forEach(vm.stations, function(station, idx) {
                  vm.stations[idx] = station;
                });
                client_deferred.resolve(station.client);
              } else {
                client_deferred.reject(null);
              }
            });
            console.log('saving client', station.client, station);
          }, function(reason) {
            console.log('dismiss', reason);
            client_deferred.reject(reason);
          });
          return client_deferred.promise;
        };

        $scope.toggleApp = function(station, app, parent_id) {
            var creative_fields = [];
            angular.copy(station.creative_fields, creative_fields);
            if (creative_fields.includes(app.slug)) {
                var index = creative_fields.indexOf(app.slug);
                creative_fields.splice(index, 1);
            } else {
                creative_fields.push(app.slug);
            }

            var doToggleApp = function(parent_id) {
              var stations_list = vm.stations;
              if (parent_id) {
                stations_list = vm.children[parent_id];
              }
              dataservice.setCreativeFields(station, creative_fields)
              .then(function(data) {
                console.log('set creative fields', data);
                var idx = stations_list.items.findIndex(function(item) { return station.id == item.id; });
                
                $scope.$applyAsync(function() {
                  stations_list.items[idx] = data;
                  logger.success('Station mise à jour :)');
                });
              }, function(error) {
                console.log('error creative fields', error);
                logger.error('Error :(');
              });
            }

            if (!app.available) {
              $translate('CREATIVE_FIELD.UNACTIVABLE').then(function(txt) {logger.info(txt)});
            } else if (!!app.remote_service && creative_fields.includes(app.slug)) {
              $scope.client(station, null, false, app.slug).then(function() {
                doToggleApp(parent_id);
              }, function() {
                $translate(app.slug.uppercase + '_PROVIDER_MISSING').then(function(txt) {logger.error(txt)});
              })
            } else {
              doToggleApp(parent_id);
            }
        };

        $scope.affiliate = function(station, parent_station) {
          dataservice.affiliateStation(station.id, parent_station.id).then(function(response) {
            logger.success('OK :)', response);
            var idx = vm.stations.items.findIndex(function(item) { return station.id == item.id; });
            $scope.$applyAsync(function() {
              refresh();
            });
          });
        };

        $scope.unaffiliate = function(station) {
          dataservice.unaffiliateStation(station.id).then(function(response) {
            logger.success('OK :)', response);
            $scope.$applyAsync(function() {
              refresh();
            });
          });
        };

        $scope.toggleStationStatus = function(station) {
          console.log('toggle station status', station.expired ? '-> active' : '-> expired', station);
          dataservice.toggleStationStatus(station, station.expired).then(function(response) {
            logger.success('OK :)', response);
            var idx = vm.stations.items.findIndex(function(item) { return station.id == item.id; });
            $scope.applyAsync(function() {
              vm.stations.items[idx] = station;
            });
          })
        };
    }
})();

(function() {
    'use strict';

    angular
        .module('app.stations')
        .run(appRun);

    appRun.$inject = ['routerHelper'];
    /*  */
    function appRun(routerHelper) {
        routerHelper.configureStates(getStates());

        function getStates() {
            return [
                {
                    state: 'stations',
                    config: {
                        url: '/stations',
                        // controller: 'StationsController',
                        // controllerAs: 'vm',
                        title: 'stations',
                        abstract: true
                    }
                },
                {
                    state: 'stations.index',
                    config: {
                        onEnter: ['$rootScope', '$state', 'myservice', '$timeout', function($rootScope, $state, myservice, $timeout) {
                          if (!myservice.isAdmin()) {
                            $timeout(function() {
                                $rootScope.$broadcast('unauthorized', $state.current);
                            });
                            return false;
                          }
                        }],
                        url: '',
                        views: {
                            '@': {
                                templateUrl: 'app/stations/stations.html',
                                controller: 'StationsController',
                                controllerAs: 'vm',
                                resolve: {
                                    services: ['myservice', 'MainService', function(myservice, MainService) {
                                        return myservice.isAuthed() && MainService.fetchApps() || {};
                                    }]
                                }
                            }
                        },
                        title: 'stations',
                        settings: {
                            nav: 3,
                            level: ['admin'],
                            content: '<span class="smartphone-hidden">Stations</span>'
                        }
                    }
                }
            ];
        }
    }
})();

angular.module('app.core').run(['$templateCache', function($templateCache) {$templateCache.put('app/admin/admin.html','<section class="main-content admin"><section class=matter><div class=container><div class=row><div class=widget><div ht-widget-header title={{vm.title}} ng-cloak></div><div class="widget-content user"><h3>Settings</h3></div><div class=widget-foot><div class=clearfix></div></div></div></div></div></section></section>');
$templateCache.put('app/common/_offer.html','<div class=code-serial><div class=serial-name title="{{ \'STATIONS.CODES_SERIAL_NAME\' | translate }}"><span class=medium-gray><translate>STATIONS.CODES_SERIAL</translate>&nbsp;{{code.serial.id}}&nbsp;:&nbsp;{{code.serial.name}}</span></div></div><div class=code-value><span class=code-usable title="{{ \'STATIONS.CODES_USED\' | translate }}"><i class="fa fa-envelope-{{code.used ? \'open-o\' : \'o\'}}"></i></span> <span class=serial-name title="{{ \'STATIONS.CODES_VALUE\' | translate }}">{{code.value}}</span></div><div class=code-properties ng-if=!!code.features><div class=code-features ng-include="\'app/common/_offer_features.html\'" ng-init="offer = code"></div><div class=code-used><div ng-if=code.used class=user><a href user-show data-user=code.user title={{code.user.name}}><img ng-src="{{code.user.avatar || \'images/default-avatar.png\'}}" class="user-avatar avatar"></a></div></div></div>');
$templateCache.put('app/common/_offer_features.html','<div ng-if=!!offer.features.creative_fields ng-repeat="(app, features) in offer.features.creative_fields" class=app-link title="{{ \'CREATIVE_FIELDS\' | translate }}"><i ng-class="\'icon-\' + app"></i> <span ng-if=!!offer.features.creative_fields[app].validity_duration title="{{ \'STATIONS.VALIDITY_DURATION\' | translate }}"><i class="fa fa-calendar"></i>{{offer.features.creative_fields[app].validity_duration | amDurationFormat : \'day\'}}</span> <span ng-if=!!offer.features.creative_fields[app].max_user_downloads title="{{ \'STATIONS.MAX_USER_DOWNLOADS\' | translate }}"><i class="fa fa-download"></i>{{offer.features.creative_fields[app].max_user_downloads}}</span></div>');
$templateCache.put('app/common/_station_summary.html','<div class="station station-summary" ng-if=station.id><div class=station-logo><img ng-src="{{station.logo || \'/images/blank_cover.png\'}}" class=logo></div><div class=station-name>{{station.name + " / " + station.address_city + ", " + station.address_country || \'STATS.ALL_STATIONS\' | translate}} <span uib-dropdown keyboard-nav=true ng-if="me.level===\'admin\' || station.allowed_to_generate_coupon_invitation_codes"><button id=action-button type=button class="btn action button" uib-dropdown-toggle title=Actions><span><i class=icon-gear></i></span></button><ul class="dropdown-menu dropdown-menu-right profiles" uib-dropdown-menu aria-labelledby=Actions><li translate-attr-title=EDIT><a href station-form data-station=station><i class="fa fa-pencil"></i>&nbsp;<translate>EDIT</translate></a></li><li translate-attr-title=STATIONS.CODES ng-if=station.allowed_to_generate_coupon_invitation_codes><a href codes-form data-station=station><i class="fa fa-id-badge"></i>&nbsp;<translate>STATIONS.CODES</translate></a></li><div class=triangle-left></div></ul></span></div></div>');
$templateCache.put('app/dashboard/_dashboard_header.html','<div class="page-head dashboard-head"><ng-include src="\'app/common/_station_summary.html\'"></ng-include><div class=tools><div uib-dropdown auto-close=always ng-if="me.level===\'admin\'" class><div><label translate>STATS.STATION</label></div><button id=station-button type=button class="btn button button-hollow" uib-dropdown-toggle ng-if=station>{{station.name}}&nbsp;<span class=caret></span></button><ul class="dropdown-menu stations" uib-dropdown-menu aria-labelledby=Stations><li><a href ng-click="vm.setStation({name: \'All\', id: \'\'})" translate>All</a></li><hr><li><input ng-if=!$index ng-model=vm.stationFilter ng-click=$event.stopPropagation() ng-model-options="{debounce: 250}" type=text class=filter-stations placeholder="{{\'FILTER\' | translate }}"></li><li ng-repeat="station in vm.stations" ng-if=station ng-click=vm.setStation(station)>{{station.name}}</li></ul></div><div uib-dropdown auto-close=always is-open=false class><div><label translate>STATS.SERVICE</label></div><button id=service-button type=button class="btn button button-hollow" uib-dropdown-toggle ng-if=vm.service ng-class="{inactive: !vm.myservice.me().creative_fields.includes(vm.service)}">{{vm.service}}&nbsp;<span class=caret></span></button><ul class="dropdown-menu services-list" uib-dropdown-menu aria-labelledby=Services><li ng-repeat="service in vm.services" ng-click=vm.setService(service) ng-if=service>{{service}}</li></ul></div><div><div><label translate>{{ga4 ? \'STATS.GA4\' : \'STATS.GA3\'}}</label></div><label class=toggle-switch><input type=checkbox ng-change=vm.toggleGA4() ng-model=ga4><div class=checkbox title="{{ ga4 ? \'STATS.GA4\' : \'STATS.GA3\' | translate }}"></div></label></div></div></div>');
$templateCache.put('app/dashboard/dashboard.html','<section id=dashboard-view class=main-content><ng-include src="\'app/layout/mainbar.html\'"></ng-include><section class="matter board" lazy-load><div ng-include="\'app/dashboard/_dashboard_header.html\'"></div><div class=container ng-cloak ng-if=graphSets.length><uib-tabset ng-if="me && me.creative_fields.length && me.creative_fields.includes(vm.service)" class=tabs active=activeJustified justified=true><uib-tab index=$index heading={{graphSet.title}} ng-repeat="graphSet in graphSets | orderBy:\'position\' track by graphSet.slug" select="selectTab($event, $index, graphSet)"><dashboard-graphset graphset=graphSet ng-cloak></dashboard-graphset></uib-tab></uib-tabset><div class=no-stats ng-cloak ng-if="me && me.creative_fields.length && !me.creative_fields.includes(vm.service)"><translate>STATS.NO_ACCESS</translate><div><a ui-sref=services class=button translate>STATS.ACCESS_SERVICES</a></div></div></div></section></section>');
$templateCache.put('app/core/404.html','<section id=dashboard-view class=main-content><section class=matter><div class=container><div class=row><div class=col-md-12><ul class=today-datas><li class=bred><div class=pull-left><i class="fa fa-warning"></i></div><div class="datas-text pull-right"><a><span class=bold>404</span></a>Page Not Found</div><div class=clearfix></div></li></ul></div></div><div class=row><div class="widget wblue"><div ht-widget-header title="Page Not Found" allow-collapse=true></div><div class="widget-content text-center text-info"><div class=container>No soup for you!</div></div><div class=widget-foot><div class=clearfix></div></div></div></div></div></section></section>');
$templateCache.put('app/layout/ht-top-nav.html','<nav class="navbar navbar-fixed-top navbar-inverse"><div class=navbar-header><a href="/" class=navbar-brand><span class=brand-title>{{vm.navline.title}}</span></a> <a class="btn navbar-btn navbar-toggle" data-toggle=collapse data-target=.navbar-collapse><span class=icon-bar></span> <span class=icon-bar></span> <span class=icon-bar></span></a></div><div class="navbar-collapse collapse"><div class="pull-right navbar-logo"><ul class="nav navbar-nav pull-right"><li class="dropdown dropdown-big"><a href=http://game.1dtouch.com target=_blank>Game</a></li><li class="dropdown dropdown-big"><a href=http://music.1dtouch.com target=_blank>Music</a></li><li class="dropdown dropdown-big"><a href=http://book.1dtouch.com target=_blank>Book</a></li></ul></div></div></nav>');
$templateCache.put('app/layout/mainbar.html','<div class=mainbar><div class=page-title>{{ vm.title }}</div><div class=page-subtitle>{{ vm.subtitle }}</div><div class=clearfix></div></div>');
$templateCache.put('app/layout/shell.html','<section id=content class="content simple-search"><div ng-include="\'app/layout/sidebar.html\'" id=sidebar-container ng-controller="SidebarController as vm"></div><div ui-view id=main-panel></div><div ngplus-overlay ngplus-overlay-delay-in=50 ngplus-overlay-delay-out=700 ngplus-overlay-animation=dissolve-animation><span class=spinner-animation ng-include="\'images/loaders/puff.svg\'"><div class="page-spinner-message overlay-message">{{vm.busyMessage}}</div></span></div></section>');
$templateCache.put('app/layout/sidebar.html','<ht-sidebar when-done-animating=vm.sidebarReady()><div class=sidebar-filler></div><header><div class="logo icon-divercities"></div></header><div class="current-user dropdown-container" uib-dropdown><span id=my-account class uib-dropdown-toggle ng-if=vm.myservice.me()><span class=user-avatar><img class=avatar ng-src="{{vm.myservice.me().avatar || \'/images/default-avatar.png\'}}" width=120 height=120></span> <span class=user-name ng-class="{empty: !vm.myservice.me().name}" title={{vm.myservice.me().name}}>{{ vm.myservice.me().name }}<i ng-if=!vm.myservice.me().name class="fa fa-code"></i></span> <span class=caret></span></span><ul class="dropdown-menu dropdown-select" uib-dropdown-menu aria-labelledby="My account"><li class=user-name ng-class="{empty: !vm.myservice.me().name}" user-show data-user=vm.myservice.me()><h4>{{ vm.myservice.me().name }}<i ng-if=!vm.myservice.me().name class="fa fa-code"></i></h4></li><li class="action 1dlab"><a href="http://1d-lab.eu/lecosysteme-1d-touch/" target=_blank translate>INDIE_CREATION</a></li><li class="action enable-tours" ng-show=get_nb_enabled_tours()><a ng-click=reinit_tours() translate>REINIT_TOURS</a></li><li class="action contact-us"><a href=mailto:hello@1dtouch.com translate>CONTACT_US</a></li><li class="action signout"><a ng-click=vm.logout() href translate>LOGOUT</a></li></ul></div><div class=sidebar-inner><ul class=navi><li class=fade-selection-animation ng-class=vm.isCurrent(r) ng-repeat="r in vm.navRoutes"><a ui-sref={{r.name}}({stationId:null}) ng-bind-html=r.settings.content></a></li></ul></div></ht-sidebar>');
$templateCache.put('app/services/_service.html','<div class=service-content><div class=service-infos><div class=service-icon><i ng-class="\'icon-\' + service.slug" class=app-icon></i></div><div class=service-details><div class=service-name data-ng-bind-html=service.name></div><div class=service-description data-ng-bind-html=service.description></div></div></div><div class=go><a href="{{service.enabled && service.authed_url || undefined}}" target=_blank title={{service.name}} ng-if="service.slug!=\'munki\'"><i class="fa fa-play"></i></a> <a ng-click=showConfirm(true) target=_blank title={{service.name}} ng-if="service.slug==\'munki\'"><i class="fa fa-play"></i></a></div></div><div class=service-actions><div class="button button-hollow resource go" ng-if="service.slug!=\'munki\'"><a href="{{service.enabled && service.authed_url || undefined}}" target=_blank title={{service.name}} translate>ACCESS_RESOURCE</a></div><div class="button button-hollow resource go" ng-if="service.slug==\'munki\'" ng-click=showConfirm(true) title={{service.name}} translate>ACCESS_RESOURCE</div><a ng-if=!service.remote_service ui-sref={{statsSref(service)}} title=Statistiques class="button stats" translate>STATIONS.STATS</a><div ng-if=service.remote_service class="button stats" type-of-service-request=stats_request service-activation-request=service>{{\'STATIONS.STATS\' | translate}}</div><div class="button button-hollow support" type-of-service-request=service_activation service-activation-request=service>{{\'HELP\' | translate}}</div></div><div id=confirmation-dialog ng-if="service.slug==\'munki\'"><div class="confirmation-dialog modal fade" role=dialog><div class=content><p class=service-description>Connectez-vous \xE0 votre application Munki avec les identifiants qui vous ont \xE9t\xE9 envoy\xE9s par mail.</p><p><em class=service-description>Identifiant munki oubli\xE9 ?<br>Suivez les instructions indiqu\xE9es dans le mail pour r\xE9initialiser votre mot de passe.</em></p><button class=button ng-click="reinvite(me, \'munki\')">Recevoir \xE0 nouveau les instructions</button></div></div></div>');
$templateCache.put('app/services/_service_activation_request.html','<div class=close-button ng-click=$ctrl.cancel()><i class=icon-plus></i></div><div class=service-contact-request><h1 class=service-contact-header><a href={{$ctrl.service.url}} target=_blank><i ng-class="\'icon-\' + $ctrl.service.slug" class=app-icon></i> {{$ctrl.service.name}}</a></h1><p class=service-contact-description>{{$ctrl.service.description}}<br><translate>SERVICES.CONTACT_REQUEST_MORE</translate><a href={{$ctrl.service.url}} target=_blank>{{$ctrl.service.url}}</a></p><hr><h4 ng-if=!$ctrl.service.enabled translate>SERVICES.NOT_ACTIVATED</h4><p ng-if="$ctrl.type==\'service_activation\'" translate>SERVICES.CONTACT_REQUEST_TEXT</p><p ng-if="$ctrl.type==\'stats_request\'" translate>SERVICES.STATS_REQUEST_TEXT</p><form class role=form name=createForm ng-submit><div class=control-group><div class><input type=email ng-model=$ctrl.email ng-model-options="{debounce: 250}" class id=email translate translate-attr-placeholder=chuck@norris.fr name=contactEmail ng-required=!$ctrl.phone> <input type=tel ng-model=$ctrl.phone ng-model-options="{debounce: 250}" class id=phone translate translate-attr-placeholder=0666666666 name=contactPhone minlength=10 ng-required=!$ctrl.email> <textarea ng-model=$ctrl.message ng-model-options="{debounce: 250}" class id=message translate translate-attr-placeholder={{$ctrl.message}} name=message row=6></textarea></div></div><div class=error-message ng-show="createForm.$submitted || createForm.contactEmail.$dirty || createForm.contactPhone.$dirty"><div ng-show=createForm.contactEmail.$error.email translate>EMAIL.INVALID</div><div ng-show=createForm.contactPhone.$error.minlength translate>PHONE.INVALID</div><div ng-show="createForm.contactEmail.$error.required || createForm.contactPhone.$error.required" translate>SERVICES.FIELD_REQUIRED</div></div><div class=submit><button class="button button-action" ng-disabled="sending || createForm.$invalid" ng-click=$ctrl.ok() translate>SEND</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></form></div>');
$templateCache.put('app/services/_services_header.html','<div class="page-head services-head"><span class=header-title translate>SERVICES.CATCH_PHRASE</span>&nbsp;</div>');
$templateCache.put('app/services/services.html','<section id=services-view class=main-content><ng-include src="\'app/layout/mainbar.html\'"></ng-include><section class=matter><div class=container><div class=row><div class=col-md-12><div class=widget><div class="widget-content services"><div ng-repeat="service in vm.services | filter:{enabled:true}" ng-include="\'app/services/_service.html\'" ng-init="service = service;" class=service ng-class="{\'inactive\': !service.enabled}"></div></div><div class=widget-foot><div class=clearfix></div></div></div></div></div><hr><div class=row><div class=col-md-12><div class=widget><div class="widget-content help"><h2 translate>SERVICES.DOCUMENTATION.TITLE</h2><div class="button button-hollow doc"><a href=https://fr.calameo.com/read/0059889785779fcfaf35c target=_blank translate>SERVICES.DOCUMENTATION.LINK</a></div></div></div></div></div><hr><div class=row><div class=col-md-12><div class=widget><div class="widget-content rgpd"><h2 translate>SERVICES.RGPD.TITLE</h2><p><a ng-href={{vm.rgpd_url}} target=_blank class="button button-hollow" translate>SERVICES.RGPD.LINK</a></p></div></div></div></div></div></section></section>');
$templateCache.put('app/sign_in/auth.html','<section class=main-content><section class=matter><div class=container><div class={{classes}} ng-if=!hide ng-cloak><header ui-view=header><div class="logo icon-divercities"></div><h2 translate>DISCOVER_NEW_AUTHORS</h2></header><div ui-view=auth></div><footer ui-view=footer></footer></div></div></section></section><div ngplus-overlay ngplus-overlay-delay-in=50 ngplus-overlay-delay-out=700 ngplus-overlay-animation=dissolve-animation><span class=spinner-animation ng-include="\'images/loaders/puff.svg\'"><div class="page-spinner-message overlay-message">{{busyMessage}}</div></span></div>');
$templateCache.put('app/sign_in/denied.html','<form class=plain-text>{{UNAUTHORIZED_MSG}}</form><form><button class=button ng-click="$state.go(\'auth.signOut\')" translate>SIGN_OUT</button></form>');
$templateCache.put('app/sign_in/sign_in.html','');
$templateCache.put('app/sign_in/sign_out.html','');
$templateCache.put('app/sign_in/sign_up.html','');
$templateCache.put('app/stations/_children_oneline.html','<a ui-sref="users.index({station: child.id})" class=station-logo><img ng-src="{{child.logo || (vm.SCHEME+vm.APP+\'.\'+vm.DC_DOMAIN+\'/images/default-avatar.png\')}}" class=logo> {{::child.id}}: {{::child.name}}</a> (<span class=count><span translate-attr-title=ACTIVE>{{ ::child.active_users }}</span> / <span translate-attr-title=TOTAL>{{ ::child.total_users }}</span></span>) <span class=child-station-action><span uib-dropdown keyboard-nav=true><button id=action-button-child-{{child.id}} type=button class=btn uib-dropdown-toggle title=Actions><span><i class="fa fa-gear"></i></span></button><ul class="dropdown-menu dropdown-menu-left profiles" uib-dropdown-menu aria-labelledby=Actions><li ng-if=myservice.isAdmin() translate-attr-title=EDIT><a href station-form data-onsuccess=onsuccess data-onerror=onerror data-station=child><i class="fa fa-pencil"></i>&nbsp;<translate>EDIT</translate></a></li><li translate-attr-title=STATIONS.CODES ng-if="child.sso_setting.provider == \'code\'"><a href codes-form data-station=child><i class="fa fa-id-badge"></i>&nbsp;<translate>STATIONS.CODES</translate></a></li><li ng-if=myservice.isAdmin() translate-attr-title=STATIONS.CLIENT><a href ng-click=client(child)><i class="fa fa-user-circle-o"></i>&nbsp;<translate>STATIONS.CLIENT</translate></a></li><li ng-if=myservice.isAdmin() translate-attr-title=STATIONS.SSO_SETTING><a href ng-click=sso_setting(child)><i class="fa fa-user-circle-o"></i>&nbsp;<translate>STATIONS.SSO_SETTING</translate></a></li><li ng-if="myservice.isAdmin() && !!child.parent_id" translate-attr-title=STATIONS.UNAFFILIATE><a href ng-click=unaffiliate(child)><i class="fa fa-child"></i>&nbsp;<translate>STATIONS.UNAFFILIATE</translate></a></li><li ng-if=myservice.isAdmin() translate-attr-title=STATIONS.STOP><a href ng-click=stop(child)><i class="fa fa-stop-circle"></i>&nbsp;<translate>STATIONS.STOP</translate></a></li></ul></span></span><br><button ng-repeat="app in vm.apps track by $index" ng-click="toggleApp(child, app, station.id)" class="app {{app.slug}}" ng-class="{\'active\': !!child.creative_fields.includes(app.slug), \'inactive\': !child.creative_fields.includes(app.slug)}" title="{{ app.name.fr || app.name.en }}"><i ng-class="\'icon-\' + app.slug" class=app-icon></i></button>');
$templateCache.put('app/stations/_station_client.html','<div class=client><form name=clientForm><div class=control-group><label for=portal_url translate>STATIONS.CLIENT.NAME</label> :<br><input type=text name=name ng-model=$ctrl.station.client.name required></div><div class=control-group ng-if="$ctrl.station.creative_fields.includes(\'munki\') || $ctrl.required_client_property == \'munki\'"><label><translate>STATIONS.CLIENT.MUNKI_PROVIDER_ID</translate>:<br><input type=number name=munki_provider_id ng-model=$ctrl.station.client.munki_provider_id ng-required="$ctrl.required_client_property == \'munki\'"></label></div><div class=control-group ng-if="$ctrl.station.creative_fields.includes(\'tenk\') || $ctrl.required_client_property == \'tenk\'"><label><translate>STATIONS.CLIENT.TENK_PROVIDER_ID</translate>:<br><input type=number name=tenk_group_id ng-model=$ctrl.station.client.tenk_group_id ng-required="$ctrl.required_client_property == \'tenk\'"></label></div><div class=submit><button class="button button-action" ng-disabled=clientForm.$invalid ng-click=$ctrl.ok() translate>SAVE</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></form></div>');
$templateCache.put('app/stations/_station_codes.html','<uib-tabset class=tabs active=activeJustified justified=true><uib-tab index=1 heading="{{\'STATIONS.GENERATE_CODES\' | translate}}"><div class=codes><h3 translate>STATIONS.CODES</h3><div class="codes-description hint" translate translate-values="{use_url: \'http://music.1dtouch.com/code\'}">STATIONS.CODES_DESCRIPTION</div><div class=codes-stat><label translate>STATIONS.CODES_QUOTA</label> :<br><span class=total-codes title="{{ \'STATIONS.GENERATED_CODES\' | translate }}">{{ $ctrl.totalCodes.meta.total }}</span> / <span class=quota>{{ $ctrl.station.coupon_invitation_codes_limit || \'&infin;\' }}</span></div><div class=codes-stat><label translate>STATIONS.REMAINING_CODES</label> (<a href ng-click="$ctrl.download({state: \'usable\'})"><i class="fa fa-download"></i></a>) :<br>{{ $ctrl.totalCodes.meta.total - $ctrl.usedCodes.meta.total }}</div><div class=codes-stat><label translate>STATIONS.USED_CODES</label> (<a href ng-click="$ctrl.download({state: \'used\'})"><i class="fa fa-download"></i></a>) :<br>{{ $ctrl.usedCodes.meta.total }}</div><hr><form name=codesForm class=codesForm ng-if="$ctrl.myservice.isAdmin() || $ctrl.station.allowed_to_generate_coupon_invitation_codes"><div class=control-field><label><translate>STATIONS.GENERATE_CODES.LABEL</translate>&nbsp;<input ng-model=$ctrl.nbCodes type=number min=1 name=nbCodes class=nb-codes required></label></div><div class=control-field><label><translate>STATIONS.GENERATE_CODES.INCLUDE_SERIAL</translate>&nbsp;<input ng-model=$ctrl.inSerial type=checkbox name=inSerial class=codes-in-serial></label></div><div class=sub-control-group><div class=control-field ng-if=$ctrl.inSerial><label><translate>STATIONS.GENERATE_CODES.SERIAL_NAME</translate>&nbsp;<input ng-model=$ctrl.serialName type=text name=serial_name required=$ctrl.inSerial></label></div></div><label translate>STATIONS.GENERATE_CODES.FEATURES</label><div class=sub-control-group><div class=control-group><label translate>STATIONS.GENERATE_CODES.CREATIVE_FIELDS</label> <label class="checkbox-field creative-field" ng-repeat="creative_field in $ctrl.apps"><input type=checkbox ng-model=$ctrl.creative_fields[creative_field] class><i ng-class="\'icon-\' + creative_field"></i>&nbsp;{{creative_field | capitalize}}</label><div class="hint pre-line" translate>STATIONS.DEFAULT_CREATIVE_FIELDS.HINT</div></div><div class=sub-control-group ng-repeat="creative_field in $ctrl.apps" ng-if=$ctrl.creative_fields[creative_field]><i ng-class="\'icon-\' + creative_field"></i><div class=control-field><label><translate>STATIONS.GENERATE_CODES.VALIDITY</translate>&nbsp;<input ng-model=$ctrl.features.creative_fields[creative_field].validity_duration type=number min=0 name=validityDuration class=validity-duration></label></div><div class=control-field ng-if="creative_field == \'game\'"><label><translate>STATIONS.GENERATE_CODES.MAX_DOWNLOADS</translate>&nbsp;<input ng-model=$ctrl.features.creative_fields[creative_field].max_user_downloads type=number min=0 name=maxUserDownloads class=max-user-downloads></label><div class="hint pre-line" translate>STATIONS.GENERATE_CODES.MAX_DOWNLOADS.HINT</div></div></div></div><button type=button ng-disabled="codesForm.$invalid || $ctrl.nbCodes<1" class="action button button-action" ng-click=$ctrl.ok() translate>GENERATE</button></form></div></uib-tab><uib-tab index=2 ng-init=$ctrl.getSerials() heading="{{\'STATIONS.CODES_SERIALS\' | translate}}"><div class="codes codes-serial"><h3 translate>STATIONS.CODES_SERIALS</h3><table class="table table-condensed table-striped"><thead><tr><th data=serial-number><translate>STATIONS.CODES_SERIAL_NUMBER</translate></th><th data=serial-name><translate>STATIONS.CODES_SERIAL_NAME</translate></th><th data=serial-count><translate>STATIONS.CODES_SERIAL_COUNT</translate></th><th data=serial-features><translate>STATIONS.CODES_SERIAL_FEATURES</translate></th></tr></thead><tbody><tr class=codes-serial ng-repeat="serial in $ctrl.serials"><td>{{serial.id}}</td><td>{{serial.name}}</td><td title="{{ \'STATIONS.CODES.REMAINING_SERIAL\' | translate }}">{{serial.remaining_items_count}}/{{serial.items_count}} <span>(<a href ng-click="$ctrl.download({serial_number: serial.number}, serial.name)"><i class="fa fa-download"></i></a>)</span></td><td><div class=code-features ng-include="\'app/common/_offer_features.html\'" ng-init="offer = serial"></div></td></tr></tbody></table></div></uib-tab><uib-tab index=3 ng-init=$ctrl.nextPage() heading="{{\'STATIONS.CODES_ALL\' | translate}}"><div class=codes-all><nav class=explore><h3><translate>STATIONS.CODES_ALL</translate>: {{ $ctrl.meta.total }}<span>(<a href ng-click=$ctrl.download_current()><i class="fa fa-download"></i></a>)</span></h3><div class=explore-nav><input type=search class=filter-field ng-model-options="{debounce: 150}" ng-model=$ctrl.filter_qry placeholder="{{ \'STATIONS.CODES.FILTER\' | translate }}"> <a ng-click="$ctrl.filter({creative_field: null, state: null})" ng-class="{\'active\': $ctrl.params.state == null && $ctrl.params.creative_field == null}"><span translate>ALL</span></a> <a ng-click="$ctrl.filter({state: \'usable\'})" ng-class="{\'active\': $ctrl.params.state == \'usable\'}"><span translate><i class="fa fa-envelope-o"></i></span></a> <a ng-click="$ctrl.filter({state: \'used\'})" ng-class="{\'active\': $ctrl.params.state == \'used\'}"><span translate><i class="fa fa-envelope-open-o"></i></span></a> <a ng-repeat="app in $ctrl.apps" ng-click="$ctrl.filter({features: {creative_fields: {app: {}}}})" class=app-link ng-class="{\'active\': $ctrl.params.creative_field == app}"><i ng-class="\'icon-\' + app"></i></a></div></nav><div class=codes><div class=code ng-repeat="code in $ctrl.codes" ng-include="\'app/common/_offer.html\'" ng-init="code = code"></div></div><a ng-if="$ctrl.codes.length < $ctrl.meta.total" class="icon-more more-codes" ng-click=$ctrl.nextPage()></a></div></uib-tab></uib-tabset>');
$templateCache.put('app/stations/_station_form.html','<form name=stationEditForm class=station-form><uib-tabset class=tabs active=activeJustified justified=true><uib-tab index=1 heading="{{\'STATIONS.INFORMATIONS\' | translate}}"><section class=content><div class="image-upload control-group"><label for=logo class translate>LOGO</label><div type=file class=form-drop-zone id=logo ngf-select ngf-drop ngf-drag-over-class="\'dragover\'" ng-model=$ctrl.station.logo ngf-pattern="\'image/*\'" accept=image/* ngf-max-size=2MB ngf-drop-available=dropSupported ng-init="initial_logo = $ctrl.station.logo"><div ng-if=$ctrl.station.logo class=preview><img ngf-src="(!$ctrl.station.logo.$error && $ctrl.station.logo)"></div><div ng-show=dropSupported class=drop-file-hint translate>DROP_FILE_HERE</div></div></div><div class=control-group><label translate>NAME</label> <input ng-model=$ctrl.station.name type=text name=name required></div><div class="description control-group"><label translate>DESCRIPTION</label> <textarea ng-model=$ctrl.station.description name=description redactor class rows=8></textarea><div class=redactor-hint><p translate>REDACTOR_HINT</p></div></div><div class=control-group><label translate>ADDRESS</label> <input ng-model=$ctrl.station.address_street type=text name=address_street></div><div class=city-wrapper><div class="control-group postal-code"><label><span translate>ZIP_CODE</span></label> <input ng-model=$ctrl.station.address_postal_code type=text name=address_postal_code></div><div class="control-group city"><label><span translate>CITY</span></label> <input ng-model=$ctrl.station.address_city type=text name=address_city></div></div><div class=control-group><label translate>COUNTRY</label><country-select ng-model=$ctrl.station.address_country cs-priorities="FR, CA, GB, ES, DE, MA"></country-select></div></section></uib-tab><uib-tab index=2 heading="{{\'STATIONS.COMMON.SETTINGS\' | translate}}" ng-if="$ctrl.station.creative_fields.length || $ctrl.station.sso_setting.provider == \'code\'"><section class=content><div class=control-group ng-if=$ctrl.myservice.isAdmin()><div class=control-group><label translate>STATIONS.MANAGER_HANDLE_VALIDITY</label> <label class=checkbox-field><input type=checkbox ng-model=$ctrl.station.managers_can_handle_validity class><translate>STATIONS.AUTHORIZE_HANDLING</translate></label><div class="hint pre-line" translate>STATIONS.MANAGER_HANDLE_VALIDITY.HINT</div></div></div><div class=control-group ng-if="$ctrl.myservice.isAdmin() || $ctrl.station.managers_can_handle_validity"><div class=control-group><label translate>STATIONS.VALIDITY_DUE_DATE</label> <label class=checkbox-field><input type=checkbox ng-model=$ctrl.station.has_validity_due_date class><translate>STATIONS.HAS_VALIDITY_DUE_DATE</translate></label></div><div class=control-group ng-show=$ctrl.station.has_validity_due_date><label class=date-field><input type=date ng-model=$ctrl.station.validity_due_date class></label><div class="hint pre-line" translate>STATIONS.VALIDITY_DUE_DATE.HINT</div></div></div><div class=control-group><div class=control-group><label translate>STATIONS.DEFAULT_CREATIVE_FIELDS</label> <label class="checkbox-field creative_fields" ng-repeat="creative_field in $ctrl.default_creative_fields"><input type=checkbox ng-model=creative_field.enabled class>{{creative_field.label | capitalize}}</label><div class="hint pre-line" translate>STATIONS.DEFAULT_CREATIVE_FIELDS.HINT</div></div><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label translate>STATIONS.FORCE_ALL_CREATIVE_FIELDS_ACCESS</label> <label class="checkbox-field force_all_creative_fields_access"><input type=checkbox ng-model=$ctrl.station.settings.force_all_creative_fields_access class><translate>STATIONS.AUTHORIZE_ACCESS</translate></label><div class="hint pre-line" translate>STATIONS.FORCE_ALL_CREATIVE_FIELDS_ACCESS.HINT</div></div></div><div class=control-group ng-if=$ctrl.myservice.isAdmin()><div class=control-group><label translate>STATIONS.COMMON.SETTINGS.AUTHORIZE_STATION_TO_GENERATE_CODES</label> <label class=checkbox-field><input type=checkbox ng-model=$ctrl.station.allowed_to_generate_coupon_invitation_codes class></label><div class="hint pre-line" translate></div></div><div class=control-group ng-if=$ctrl.station.allowed_to_generate_coupon_invitation_codes><label translate>STATIONS.COMMON.SETTINGS.CODES_LIMIT</label> <label class=number-field><input type=number ng-model=$ctrl.station.coupon_invitation_codes_limit class></label><div class="hint pre-line" translate>STATIONS.COMMON.SETTINGS.CODES_LIMIT.HINT</div></div><div class=control-group><label translate>STATIONS.COMMON.SETTINGS.AUTHORIZE_STATION_TO_USE_CODES</label> <label class=checkbox-field><input type=checkbox ng-model=$ctrl.station.allowed_to_use_coupon_invitation_codes class></label><div class="hint pre-line" translate></div></div></div><div class=control-group ng-if=$ctrl.myservice.isAdmin()><div class=control-group><label translate>STATIONS.COMMON.SETTINGS.MANAGERS_CAN_TO_SOFT_DELETE</label> <label class=checkbox-field><input type=checkbox ng-model=$ctrl.station.settings.managers_can_soft_delete_users class></label><div class="hint pre-line" translate>STATIONS.COMMON.SETTINGS.MANAGERS_CAN_TO_SOFT_DELETE.HINT</div></div><div class=control-group><label translate>STATIONS.COMMON.SETTINGS.SEND_MANAGERS_NOTIFICATIONS</label> <label class=checkbox-field><input type=checkbox ng-model=$ctrl.station.settings.send_managers_notifications class></label><div class="hint pre-line" translate>STATIONS.COMMON.SETTINGS.SEND_MANAGERS_NOTIFICATIONS.HINT</div></div></div><div class="invitation-text control-group" ng-if="$ctrl.station.sso_setting.provider == \'code\' || $ctrl.station.has_children"><label translate>STATIONS.INVITATION_TEXT</label><div class="hint pre-line" translate>STATIONS.INVITATION_TEXT.HINT</div><textarea ng-model=$ctrl.station.settings.invitation_text name=invitation-text redactor class rows=8></textarea><div class=redactor-hint><p translate>REDACTOR_HINT</p></div></div><div class="welcome-text control-group" ng-if="$ctrl.station.sso_setting.provider == \'code\' || $ctrl.station.has_children"><label translate>STATIONS.WELCOME_TEXT</label><div class="hint pre-line" translate>STATIONS.WELCOME_TEXT.HINT</div><textarea ng-model=$ctrl.station.settings.welcome_text name=welcome-text redactor class rows=8></textarea><div class=redactor-hint><p translate>REDACTOR_HINT</p></div></div></section></uib-tab><uib-tab index=3 heading="{{\'STATIONS.MUSIC.SETTINGS\' | translate}}" ng-if="$ctrl.station.creative_fields.includes(\'music\')"><section class=content><div class=control-group><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label for translate>STATIONS.STATS</label><div class="control-group stats-factor-wrapper"><input class=factor type=text ng-model=$ctrl.station.settings.stats_factor></div><div class=hint translate>STATIONS.STATS.FACTOR</div></div></div><div class=control-group><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label for translate>STATIONS.UI</label><div class="control-group ui-wrapper"><label><input class=ui type=radio ng-model=$ctrl.station.settings.ui value=v2>v2</label> <label><input class=ui type=radio ng-model=$ctrl.station.settings.ui value=v3>v3</label></div><div class=hint translate>STATIONS.UI.HINT</div></div></div></section></uib-tab><uib-tab index=4 heading="{{\'STATIONS.GAME.SETTINGS\' | translate}}" ng-if="$ctrl.station.creative_fields.includes(\'game\')"><section class=content><div class=control-group><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label for translate>STATIONS.ACCESS</label> <input class="check_portal_account_validity checkbox-field" type=checkbox ng-model=$ctrl.station.settings.check_portal_account_validity><div class=hint translate>STATIONS.CHECK_PORTAL_ACCOUNT_VALIDITY.HINT</div></div></div><div class=control-group><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label for translate>STATIONS.PERIOD</label><div class="control-group period-wrapper"><input class=duration type=range data-momentduration=months min=-1 max=24 max-unit ng-model=$ctrl.station.settings.limits_period><div class=period-visu title="{{ $ctrl.station.settings.limits_period }} {{ \'MONTHS\' | translate | lowercase }}">{{ $ctrl.station.settings.limits_period }} {{ \'MONTHS\' | translate | lowercase }}</div></div><div class=hint>P\xE9riode pendant laquelle les limites ci-dessous s\'appliquent, et \xE0 l\'issue de laquelle elles se renouvellent.</div></div><div class=columns-wrapper><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label translate>STATIONS.MAX_STATION_DOWNLOADS</label> <input ng-model=$ctrl.station.settings.max_station_downloads type=number name=max_station_downloads min=-1 max placeholder="{{ \'STATIONS.UNLIMITED\' | translate }}"></div><div class=control-group><label translate>STATIONS.MAX_USER_DOWNLOADS</label> <input ng-model=$ctrl.station.settings.max_user_downloads type=number name=max_user_downloads required min="{{ ::$ctrl.station.settings.max_station_downloads>0 ? 0 : -1 }}" max="{{ $ctrl.station.settings.max_station_downloads }}"><div class="hint pre-line" translate translate-values="{max_station_downloads: max_station_downloads_deal}">STATIONS.MAX_USER_DOWNLOADS.HINT</div></div></div></div><div class=control-group><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label for translate>STATIONS.STATS</label><div class="control-group stats-factor-wrapper"><input class=factor type=text ng-model=$ctrl.station.settings.game_stats_factor></div><div class=hint translate>STATIONS.STATS.FACTOR</div></div></div></section></uib-tab><uib-tab index=5 ng-repeat="creative_field in $ctrl.station.creative_fields" heading="{{creative_field | capitalize}}" ng-if="$ctrl.station.creative_field_accesses[creative_field].remote && $ctrl.station.plans && $ctrl.station.plans[creative_field] && $ctrl.station.plans[creative_field].length"><section class=content><div class=control-group><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label class=control-group-title translate>STATIONS.SUBSCRIPTION</label><div class="control-group subscription-wrapper" ng-repeat="subscription in $ctrl.station.plans[creative_field]"><label class=subscription><input name="{{\'plan_\'+subscription.id}}" type=checkbox ng-model="$ctrl[creative_field+\'_subscription_ids\'][subscription.id.toString()]"> {{subscription.name[0].value}}</label></div><div class=hint>Abonnement <em>{{creative_field}}</em> : formule d\'acc\xE8s au catalogue.</div></div></div></section></uib-tab></uib-tabset><button type=button class="action button button-action" ng-disabled="sending || stationEditForm.$invalid" ng-click=$ctrl.ok() translate>{{ $ctrl.station.id ? \'SAVE_MODIFICATIONS\' : \'STATIONS.ADD_PLACE\' }}</button> <span class="error hint" ng-if=stationEditForm.$invalid><i class="fa fa-exclamation-circle" title="{{\'FORM.BAD_VALUES\' | translate}}"><small class translate>{{\'FORM.BAD_VALUES\' | translate}}</small></i></span></form>');
$templateCache.put('app/stations/_station_oneline.html','<td>{{::station.id}}</td><td class=station-name><a ui-sref="users.index({station: station.id})" class=station-logo><img ng-src="{{::station.logo || \'images/default-avatar.png\'}}" class=logo> <span class=name>{{::station.name}}</span></a> (<span class=count><span translate-attr-title=ACTIVE>{{ station.active_users }}</span> / <span translate-attr-title=TOTAL>{{ station.total_users }}</span></span>)<br><div class=creative-fields-toggle><button ng-repeat="app in vm.apps track by $index" ng-click="toggleApp(station, app)" class="app {{app.slug}}" ng-class="{\'inactive\': !station.creative_fields.includes(app.slug), \'unactivable\': !app.available}" title="{{ app.slug | uppercase | translate }}"><i ng-class="\'icon-\' + app.slug" class=app-icon></i></button></div><div ng-repeat="child in vm.children[station.id].items" class=child-station ng-include="\'app/stations/_children_oneline.html\'"></div></td><td class=status ng-class="{\'active\': !station.expired, \'expired\': station.expired}"><i ng-class="{\'fa-check\': !station.expired, \'fa-minus-circle\': station.expired}" class=fa ng-attr-title="{{station.expired ? \'EXPIRED\' : \'ACTIVE\' | translate}}"></i></td><td>{{::station.address_city}}</td><td>{{::station.address_country}}</td><td class=stations-action><span uib-dropdown keyboard-nav=true><button id=action-button-{{::station.id}} type=button class=btn uib-dropdown-toggle title=Actions><span><i class="fa fa-gear"></i></span></button><ul class="dropdown-menu dropdown-menu-left profiles" uib-dropdown-menu aria-labelledby=Actions><div class=triangle-right></div><li ng-if=myservice.isAdmin() translate-attr-title=EDIT><a href station-form data-station=station data-onsuccess=refresh><i class="fa fa-pencil"></i>&nbsp;<translate>EDIT</translate></a></li><li translate-attr-title=STATIONS.CODES ng-if="myservice.isAdmin() || station.allowed_to_generate_coupon_invitation_codes"><a href codes-form data-station=station><i class="fa fa-id-badge"></i>&nbsp;<translate>STATIONS.CODES</translate></a></li><li ng-if=myservice.isAdmin() translate-attr-title=STATIONS.CLIENT><a href ng-click=client(station)><i class="fa fa-user-circle-o"></i>&nbsp;<translate>STATIONS.CLIENT</translate></a></li><li ng-if=myservice.isAdmin() translate-attr-title=STATIONS.SSO_SETTING><a href ng-click=sso_setting(station)><i class="fa fa-user-circle-o"></i>&nbsp;<translate>STATIONS.SSO_SETTING</translate></a></li><li ng-if="myservice.isAdmin() && !vm.children[station.id] && !station.parent_id" translate-attr-title=STATIONS.AFFILIATE_TO><a href station-choose=affiliate choice-arguments=station><i class="fa fa-child"></i>&nbsp;<translate>STATIONS.AFFILIATE_TO</translate></a></li><li ng-if="myservice.isAdmin() && !vm.children[station.id]" translate-attr-title=STATIONS.STOP><a href ng-click=toggleStationStatus(station)><i class="fa fa-stop-circle"></i>&nbsp;<translate>STATIONS.STOP</translate></a></li></ul></span></td>');
$templateCache.put('app/stations/_station_sso_setting.html','<div class=sso-setting><form name=ssoSettingForm><div class=control-group><label translate>STATIONS.SSO_SETTING.PROVIDER</label> :<br><select name=provider ng-model=$ctrl.station.sso_setting.provider ng-options="option for option in $ctrl.providers" required></select></div><div class=control-group><label for=portal_url translate>STATIONS.SSO_SETTING.PORTAL_URL</label> :<br><input type=text name=portal_url ng-model=$ctrl.station.sso_setting.portal_url></div><fieldset class=control-group><div ng-if=$ctrl.casProviders.includes($ctrl.station.sso_setting.provider)><div class=control-fields><label><translate>Server URL</translate>: <input name=cas_url ng-model=$ctrl.station.sso_setting.data.cas_url required></label> <label><translate>Login url</translate>: <input name=cas_login_url ng-model=$ctrl.station.sso_setting.data.cas_login_url></label> <label><translate>Service Validate URL</translate>: <input name=cas_service_validate_url value=/serviceValidate ng-model=$ctrl.station.sso_setting.data.cas_service_validate_url></label> <label><translate>Disable SSL verification</translate>: <input type=checkbox name=cas_disable_ssl_verification ng-model=$ctrl.station.sso_setting.data.cas_disable_ssl_verification></label></div></div><div ng-if="$ctrl.inforProviders.includes($ctrl.station.sso_setting.provider) || $ctrl.bmGrenobleProviders.includes($ctrl.station.sso_setting.provider) || $ctrl.decalogProviders.includes($ctrl.station.sso_setting.provider) || $ctrl.pmbProviders.includes($ctrl.station.sso_setting.provider)"><div class=control-fields><label><translate>Webservice URL</translate>: <input name=webservice_url ng-model=$ctrl.station.sso_setting.data.webservice_url required></label></div></div><div ng-if=$ctrl.pmbProviders.includes($ctrl.station.sso_setting.provider)><div class=control-fields><label><translate>Webservice Secret Key</translate>: <input name=webservice_secret_key ng-model=$ctrl.station.sso_setting.data.webservice_secret_key required></label></div></div><div ng-if=$ctrl.ipProviders.includes($ctrl.station.sso_setting.provider)><div class=control-fields><label><translate>IPs \xE0 autoriser (s\xE9par\xE9es par des virgules)</translate>: <input name=whitelisted_ips ng-model=$ctrl.station.sso_setting.data.whitelisted_ips></label> <label><translate>IPs \xE0 refuser (s\xE9par\xE9es par des virgules)</translate>: <input name=blacklisted_ips ng-model=$ctrl.station.sso_setting.data.blacklisted_ips></label></div></div><div ng-if="$ctrl.station.sso_setting.provider==\'oauth\'"><div class=control-fields><label><translate>OAUTH_CLIENT_ID</translate>: <input name=client_id ng-model=$ctrl.station.sso_setting.data.client_id></label> <label><translate>OAUTH_CLIENT_SECRET</translate>: <input name=client_secret ng-model=$ctrl.station.sso_setting.data.client_secret></label> <label><translate>OAUTH_CLIENT_OPTIONS</translate>: <textarea name=client_options ng-model=$ctrl.station.sso_setting.data.client_options></textarea></label> <label><translate>OAUTH_SCOPE</translate>: <textarea name=scope ng-model=$ctrl.station.sso_setting.data.scope></textarea></label></div></div></fieldset><div class=control-group><label><translate>STATIONS.SSO_SETTING.BIBID</translate>:<br><input type=text name=bibid ng-model=$ctrl.station.sso_setting.bibid></label><div class=control-group ng-if="$ctrl.station.sso_setting.provider && $ctrl.station.sso_setting.bibid"><div translate>STATIONS.SSO_LINK</div><div ng-if="$ctrl.station.sso_setting.provider==\'assa\'"><div>Erm\xE8s</div><div ng-repeat="app in $ctrl.station.creative_fields"><strong>{{app}} :</strong> {{$ctrl.station.sso_setting.portal_url}}/sarasvati/ws/secure/redirect.ashx?INSTANCE=EXPLOITATION&url=https%3A%2F%2Faccounts.divercities.eu%2Fusers%2Fauth%2Fassa%3Fscope%3D{{app}}%26bibid%3D{{$ctrl.station.sso_setting.bibid}}</div><div>Syracuse</div><div ng-repeat="app in $ctrl.station.creative_fields"><strong>{{app}} :</strong> {{$ctrl.station.sso_setting.portal_url}}/sarasvati/ws/secure/redirect.ashx?url=https%3A%2F%2Faccounts.divercities.eu%2Fusers%2Fauth%2Fassa%3Fscope%3D{{app}}%26bibid%3D{{$ctrl.station.sso_setting.bibid}}</div></div><div ng-if="$ctrl.station.sso_setting.provider!=\'assa\'"><div ng-repeat="app in $ctrl.station.creative_fields"><strong>{{app}} :</strong> {{$ctrl.accountsBaseUrl()}}users/auth/{{$ctrl.station.sso_setting.provider}}?scope={{app}}&bibid={{$ctrl.station.sso_setting.bibid}}</div></div></div><div class=control-group><label><translate>STATIONS.SSO_SETTING.USER_CAN_CHOOSE_PASSWORD</translate>:<br><input type=checkbox name=user_can_choose_password ng-model=$ctrl.station.sso_setting.user_can_choose_password></label></div><div class=control-group><label><translate>STATIONS.SSO_SETTING.CHECK_USER_VALIDITY_AT_SIGNIN</translate>:<br><input type=checkbox name=verify_user_validity_on_sign_in ng-model=$ctrl.station.sso_setting.verify_user_validity_on_sign_in></label></div><div class=submit><button class="button button-action" ng-disabled=ssoSettingForm.$invalid ng-click=$ctrl.ok() translate>SAVE</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></div></form></div>');
$templateCache.put('app/stations/_stations_choose.html','<form class role=form name=createForm ng-submit><div class=control-group><label for=name class=control-field translate>STATIONS</label><div class><input type=search ng-model=$ctrl.query ng-model-options="{debounce: 250}" class id=name translate translate-attr-placeholder=FILTER_PLACEHOLDER></div></div><div class><ul><li ng-repeat="station in $ctrl.stations"><a href ng-click=$ctrl.ok(station)>{{ station.name }}</a> <i ng-if="$ctrl.user.station.id == station.id" class="fa fa-check"></i></li></ul></div></form>');
$templateCache.put('app/stations/_stations_header.html','<div class="page-head stations-head"><span class=header-title translate>STATIONS</span>&nbsp; <span class=badge>{{vm.stations.meta.total || \'?\'}}</span> <span class=actions><div></div><div ng-if=myservice.isAdmin() class="action button button-action" station-form data-station=dataservice.station data-onsuccess=vm.onsuccess data-onerror=vm.onerror><translate>STATIONS.ADD_STATION</translate></div></span></div>');
$templateCache.put('app/stations/stations.html','<section id=stations-view class=main-content><ng-include src="\'app/layout/mainbar.html\'"></ng-include><section class=matter><div ng-include="\'app/stations/_stations_header.html\'"></div><div class=container><div class=row><div class=col-md-12><input type=search results=2 class="filter search-input" placeholder="{{\'FILTER\' | translate}}" ng-model=vm.searchString ng-keypress=search($event)> <label><input type=checkbox name=search_all ng-model=vm.search_all ng-value=true on-change=search()><translate>STATIONS.SEARCH_ALL</translate></label><div ng-repeat="fltr in filters">{{fltr.query}}</div></div></div><div class="row stations-list"><div class=col-md-12><div class="widget stations"><div class="widget-content text-center"><table class="table table-condensed table-striped"><thead><tr sortable-columns sort-params=vm.sortParams><th data=id><translate>STATIONS.ID</translate></th><th data=name><translate>STATIONS.NAME</translate></th><th data=status><translate>STATIONS.STATUS</translate></th><th data=address_city><translate>STATIONS.CITY</translate></th><th data=address_country><translate>STATIONS.COUNTRY</translate></th><th translate>STATIONS.ACTION</th></tr></thead><tbody><tr ng-repeat="station in vm.stations.items track by station.id" ng-include="\'app/stations/_station_oneline.html\'" class=station></tr></tbody></table></div><div class=widget-foot><div class=clearfix></div></div></div></div></div><div class="button more-button icon-more" ng-hide="vm.disabled || vm.loading" ng-click="vm.nextPage(false, true)"></div></div></section></section>');
$templateCache.put('app/users/_freeze_inactive.html','<form class role=form name=createForm ng-submit><div class=control-group><label for=name class=control-field translate>FREEZE</label><div class><translate>USERS.FREEZE_FROM</translate>&nbsp;<input type=number ng-model=$ctrl.monthsAgo required class=months id=name translate translate-attr-placeholder=PERIOD_PLACEHOLDER>&nbsp;<translate>USERS.MONTHS</translate></div></div><div class=control-group ng-show=$ctrl.myservice.isAdmin()><label for=station_id class translate>STATION_ID</label><div class><input type=text ng-model=$ctrl.stationId required class id=station_id translate translate-attr-placeholder=STATION_ID_PLACEHOLDER></div></div><div class=submit><button class="button button-action" ng-disabled=sending ng-click=$ctrl.ok() translate>OK</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></form>');
$templateCache.put('app/users/_invite_multiple.html','<form class=invite-multiple role=form name=createForm ng-submit enctype=multipart/form-data><div class="image-upload control-group"><label for=station_csv_file class translate>CSV_FILE</label><div type=file class=form-drop-zone ngf-select="console.log(\'select\', this)" ngf-drop ngf-drag-over-class="\'dragover\'" ng-model=$ctrl.invitation.station_csv_file ngf-pattern="\'text/*\'" accept=text/* ngf-max-size=5MB ngf-drop-available=dropSupported><div ng-show=dropSupported class=drop-file-hint translate>DROP_FILE_HERE</div><div ng-if=!!$ctrl.invitation.station_csv_file.type class=preview><i class=fa ng-class="{\'fa-check-circle\': [\'text/tsv\',\'text/csv\',\'text/tab-separated-values\',\'text/comma-separated-values\'].includes($ctrl.invitation.station_csv_file.type), \'fa-question-circle orange\': ![\'text/tsv\',\'text/csv\',\'text/tab-separated-values\',\'text/comma-separated-values\'].includes($ctrl.invitation.station_csv_file.type)}"><div>{{$ctrl.invitation.station_csv_file.name}}</div></i></div></div></div><div class=control-group ng-show=$ctrl.myservice.isAdmin()><label for=station_id class translate>STATION_ID</label><div class><input type=text ng-model=$ctrl.station_id required class id=station_id translate translate-attr-placeholder=STATION_ID_PLACEHOLDER></div></div><p class=hint>Les informations contenues dans chaque ligne du fichier doivent \xEAtre s\xE9par\xE9es par une tabulation<br>La premi\xE8re ligne sert \xE0 nommer les champs et doit contenir au moins un champ <em>email</em>. Les lignes suivantes doivent correspondre aux informations des utilisateurs \xE0 inviter (une ligne par utilisateur).</p><div class=control-group><label for=station_invitation_message class translate>STATIONS_INVITATION_MESSAGE_LABEL</label><div class><textarea type=text ng-model=$ctrl.invitation.station_invitation_message required redactor id=station_invitation_message>\n    </textarea></div><div class=redactor-hint><p>S\xE9lectionner du texte pour le mettre en forme</p><p>Vous pouvez \xE9galement utiliser les variables suivantes :<dl><dt>[[user]]</dt><dd>nom d\'utilisateur</dd><dt>[[email]]</dt><dd>email de l\'utilisateur</dd></dl></p></div></div><p></p><p class=hint>Le message sera suivi d\'un bouton permettant \xE0 l\'utilisateur d\'accepter son invitation</p><strong>Les utilisateurs seront automatiquement invit\xE9s avec le niveau d\'autorisation <em>membres</em></strong><p class=hint>Un membre peut voir les capsules du lieu et en sugg\xE9rer parmi les siennes.</p><div class=submit><button class="button button-action" ng-disabled=sending ng-click=$ctrl.ok() translate>SEND</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></form>');
$templateCache.put('app/users/_invite_one.html','<form class=invite-one role=form name=createForm ng-submit><div class=control-group><label for=email class translate>EMAIL_ADDRESS</label><div class><input type=text ng-model=$ctrl.user.email required class id=email translate translate-attr-placeholder=EMAIL_ADDRESS_PLACEHOLDER></div></div><div class=control-group><label for=name class translate>NAME</label><div class><input type=text ng-model=$ctrl.user.name required class id=name translate translate-attr-placeholder=NAME_PLACEHOLDER></div></div><div class=control-group ng-if=$ctrl.station.managers_can_handle_validity><label for=validity_due_date class translate>VALIDITY_DUE_DATE</label><div class><input type=date ng-model=$ctrl.user.expires_at class id=validity_due_date translate><div class="hint pre-line" translate>VALIDITY_DUE_DATE.HINT</div></div></div><div class=control-group ng-if=$ctrl.myservice.isAdmin()><div class=station-id><label class translate>STATION_ID</label> <input type=text ng-model=$ctrl.user.station_id required class id=station_id translate translate-attr-placeholder=STATION_ID_PLACEHOLDER></div></div><div class=control-group ng-if=$ctrl.myservice.isAdmin()><label class translate>LEVEL</label><div class=level><label><input type=radio name=level ng-model=$ctrl.user.level class value=user translate required checked>user</label> <label><input type=radio name=level ng-model=$ctrl.user.level class value=manager translate>manager</label></div></div><div class=control-group><div class=creative-fields ng-if=$ctrl.creative_fields.length><label><translate>CREATIVE_FIELDS_INVITATION</translate>:</label> <label class><div><input type=checkbox name=creative_fields ng-model=$ctrl.user.creative_fields ng-true-value=$ctrl.creative_fields ng-false-value=[] class> <span ng-repeat="app in $ctrl.availableApps track by app.slug | orderBy:position">{{app.name.fr || app.name.en}}{{$last?\'\':\', \'}}</span></div></label><div class="hint pre-line" translate>CREATIVE_FIELDS.HINT</div></div><div class=creative-fields ng-if=$ctrl.munki><label class><input type=checkbox name=creative_field_munki ng-model=$ctrl.user.creative_field_munki ng-value=true class><translate>CREATIVE_FIELDS_MUNKI_CREATION</translate></label><div class="hint pre-line" translate>CREATIVE_FIELDS_MUNKI.HINT</div></div></div><div class=submit><button class="button button-action" ng-disabled="sending || !$ctrl.ok" ng-click=$ctrl.ok() translate>{{$ctrl.munki ? \'USERS.CREATE\' : \'USERS.INVITE\'}}</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></form>');
$templateCache.put('app/users/_reinvite.html','<form class role=form name=createForm ng-submit><div class=control-group><label for=name class=control-field translate>USERS.REINVITE</label><div class><translate ng-if=$ctrl.station.id translate-values="{station: $ctrl.station}">USERS.REINVITE_CONFIRM</translate><translate ng-if=!$ctrl.station.id>USERS.REINVITE_UNAVAILABLE</translate></div></div><div class=submit><button class="button button-action" ng-if=$ctrl.station.id ng-disabled=sending ng-click=$ctrl.ok() translate>OK</button> <button class="button button-action" ng-disabled=sending ng-click=$ctrl.cancel() translate>CANCEL</button></div></form>');
$templateCache.put('app/users/_user.html','<h2>{{ $ctrl.user.name }}</h2><uib-tabset class=tabs active=activeJustified justified=true><uib-tab index=1 heading="{{\'USER.INFORMATIONS\' | translate}}"><div class=user-details-form><section><form><div class=control-field><label for=name translate>NAME</label> <input type=text name=name value={{::$ctrl.user.name}}></div><div class=control-field><label for=email translate>EMAIL_ADDRESS</label> <input type=text name=email translate-attr-placeholder={{EMAIL_PLACEHOLDER}} value={{::$ctrl.user.email}}></div></form><form ng-show="$ctrl.isAdmin || ( $ctrl.station.managers_can_handle_validity && $ctrl.user.level == \'user\' )"><div class=control-field><label for=validity_due_date class translate>VALIDITY_DUE_DATE</label> <input type=datetime-local ng-model=$ctrl.user.expires_at class id=validity_due_date translate><div><label></label><span class="hint pre-line" translate>VALIDITY_DUE_DATE.HINT</span></div></div><div class=control-field><label for=created_at class translate>CREATED_AT</label> <input type=datetime-local ng-model=$ctrl.user.created_at readonly class id=created_at translate></div><div class=control-field><label for=updated_at class translate>UPDATED_AT</label> <input type=datetime-local ng-model=$ctrl.user.updated_at readonly class id=updated_at translate></div><div class=control-field><label for=renewed_at class translate>RENEWED_AT</label> <input type=datetime-local ng-model=$ctrl.user.renewed_at readonly class id=renewed_at translate></div><div class=control-field><label for=last_sign_in class translate>LAST_SIGN_IN</label> <input type=datetime-local ng-model=$ctrl.user.last_sign_in_at readonly class id=last_sign_in translate></div><div class=submit ng-if="$ctrl.isAdmin || ( $ctrl.station.managers_can_handle_validity && $ctrl.user.level == \'user\' )"><button class="button button-action" ng-disabled=sending ng-click=$ctrl.update() translate>USERS.UPDATE</button> <button class="button button-action" ng-disabled=sending ng-click=$ctrl.update_validity() translate>USERS.UPDATE_VALIDITY</button> <button class="button button-action" ng-disabled=sending ng-click=$ctrl.renew() translate>USERS.RENEW</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></form></section><hr><section><div class=control-group ng-if=$ctrl.myservice.isAdmin()><div class=control-group><label for translate>USERS.UI</label><div class="control-group ui-wrapper"><label><input class=ui type=radio ng-model=$ctrl.user.settings.ui value=v2>v2</label> <label><input class=ui type=radio ng-model=$ctrl.user.settings.ui value=v3>v3</label></div><div class=hint translate>USERS.UI.HINT</div></div><div class=submit><button class="button button-action" ng-disabled=sending ng-click=$ctrl.update_settings() translate>USERS.UPDATE_SETTINGS</button> <span ng-if=sending class=loader ng-include="\'images/loaders/puff.svg\'"></span></div></div></section><hr><section><h4 translate>USER.CREATIVE_FIELDS</h4><div class=creative-field ng-repeat="creative_field_slug in $ctrl.user.creative_fields"><i class=creative-field-icon ng-class="\'icon-\' + creative_field_slug"></i> <span ng-if=!!$ctrl.user.creative_field_accesses[creative_field_slug].settings.validity_due_date title="{{ \'USER.CREATIVE_FIELD_VALIDITY_DUE_DATE\' | translate }}"><i class="fa fa-calendar"></i> {{$ctrl.user.creative_field_accesses[creative_field_slug].settings.validity_due_date | amDateFormat:\'DD / MM / YYYY\'}}</span> <span ng-if=!!$ctrl.user.creative_field_accesses[creative_field_slug].settings.max_user_downloads title="{{ \'USER.CREATIVE_FIELD_MAX_USER_DOWNLOADS\' | translate }}"><i class="fa fa-download"></i> {{$ctrl.user.creative_field_accesses[creative_field_slug].settings.max_user_downloads}}</span></div></section><hr><section><h4 translate>USERS.LISTENING</h4><div class=heatmap-container cellsize=14 ng-heatmap start="{{ ::$ctrl.startDate }}" data-url="{{ ::$ctrl.heatmapUrl }}"></div></section></div></uib-tab><uib-tab index=2 ng-init=$ctrl.getOffers() heading="{{\'USER.OFFERS\' | translate}}"><div class=codes-all><nav class=explore><h3><translate>USER.OFFERS_ALL</translate>: {{ $ctrl.codes.length }}</h3><div class=explore-nav><input type=search class=filter-field ng-model-options="{debounce: 150}" ng-model=$ctrl.filter_qry placeholder="{{ \'STATIONS.CODES.FILTER\' | translate }}"> <a ng-click="$ctrl.filter({ creative_field: null, state: null })" ng-class="{\'active\': $ctrl.params.state == null && $ctrl.params.creative_field == null}"><span translate>ALL</span></a> <a ng-repeat="app in $ctrl.apps" ng-click="$ctrl.filter({creative_field: app})" class=app-link ng-class="{\'active\': $ctrl.params.creative_field == app}"><i ng-class="\'icon-\' + app"></i></a></div></nav><div class=codes><div class=code ng-repeat="code in $ctrl.codes" ng-include="\'app/common/_offer.html\'" ng-init="code = code"></div></div><a ng-if="$ctrl.codes.length < $ctrl.meta.total" class="icon-more more-codes" ng-click=$ctrl.nextPage()></a></div></uib-tab></uib-tabset>');
$templateCache.put('app/users/_users_header.html','<div class="page-head users-head"><ng-include src="\'app/common/_station_summary.html\'"></ng-include><div class=actions><div class=filters><div ng-repeat="fltr in filters">{{fltr.query}}</div></div><div><div ng-if=vm.myservice.isAdmin() class="action button button-action" user-clean data-station=vm.dataservice.station data-onsuccess=onsuccess data-onerror=onerror translate>USERS.CLEAN</div><div class="action button button-action" user-invite-one vm=vm onsuccess=onsuccess onerror=onerror translate>USERS.CREATE</div><div ng-if=vm.myservice.isAdmin() class="action button button-action" user-invite-multiple data-station=vm.dataservice.station data-onsuccess=onsuccess data-onerror=onerror translate>USERS.CREATE.MULTIPLE</div><div class="action button button-action" ng-if=vm.myservice.isAdmin() user-export data-station=vm.dataservice.station data-filter=vm.filter data-onsuccess=onsuccess data-onerror=onerror title="{{\'USERS.EXPORT.TOOLTIP\' | translate}}" translate>USERS.EXPORT</div></div></div></div>');
$templateCache.put('app/users/users.html','<section id=users-view class=main-content infinite-scroll=vm.nextPage() infinite-scroll-disabled="vm.fetching || vm.disabled" infinite-scroll-distance=0><ng-include src="\'app/layout/mainbar.html\'"></ng-include><section class=matter><div ng-include="\'app/users/_users_header.html\'"></div><div class=container><div class=row><div class=col-md-5><input type=search results=2 class="filter search-input" placeholder="{{\'FILTER\' | translate}}" ng-keypress=search($event) ng-model=vm.searchString></div><div class=col-md-7><div class=users><span class="widget filter"><select ng-model=selectedFilter ng-change=onSelectedChange() class="filter button button-hollow"><option value>{{\'EVERYONE\' | translate}}{{vm.myservice.isAdmin() && \' (\'+vm.usersTotals.total+\')\' || undefined}}</option><option ng-if=vm.filter ng-value=vm.filter>{{vm.filter}}</option><option value=manager>{{\'MANAGERS\' | translate}}{{vm.myservice.isAdmin() && \' (\'+vm.usersTotals.managers+\')\' || undefined}}</option><option value=user>{{\'MEMBERS\' | translate}}{{vm.myservice.isAdmin() && \' (\'+vm.usersTotals.users+\')\' || undefined}}</option><option value=invited>{{\'USERS.INVITATIONS_SENT\' | translate}}{{vm.myservice.isAdmin() && \' (\'+vm.usersTotals.invited+\')\' || undefined}}</option><option ng-if=vm.myservice.isAdmin() value=active>{{\'USERS.ACTIVE_ACCOUNTS\' | translate}}&nbsp;({{vm.usersTotals.active}})</option><option ng-if=vm.myservice.isAdmin() value=frozen>{{\'USERS.FROZEN_ACCOUNTS\' | translate}}&nbsp;({{vm.usersTotals.frozen}})</option><option ng-if=vm.myservice.isAdmin() ng-repeat="app in vm.availableApps track by app.slug | orderBy:position" ng-value=app.slug>{{app.name.fr||app.name.en}}&nbsp;({{vm.usersTotals[app.slug]}})</option></select></span></div></div></div><table class="table table-condensed table-striped"><thead><tr sortable-columns sort-params=vm.sortParams><th data=name><translate>USERS.NAME</translate><span class=sort-mark></span></th><th data=id class=users-col-id><translate>USERS.ID</translate><span class=sort-mark></span></th><th data=status class=users-status><translate>USERS.STATUS</translate><span class=sort-mark></span></th><th data=level class=users-level ng-if=vm.myservice.isAdmin()><translate>USERS.LEVEL</translate><span class=sort-mark></span></th><th class="users-station smartphone-hidden" ng-if=vm.myservice.isAdmin()><translate>USERS.STATION</translate></th><th class=users-status ng-repeat="app in vm.availableApps | orderBy:\'position\'">{{app.name.fr||app.name.en}}</th><th class=users-actions><translate>USERS.ACTION</translate></th></tr></thead><tbody><tr ng-repeat="user in vm.users.items track by user.id" class=user><td class=users-col-summary><a href user-show data-user=user class=user-link><img ng-src="{{user.avatar || (vm.SCHEME+vm.APP+\'.\'+vm.DC_DOMAIN+\'/images/default-avatar.png\')}}" class="user-avatar avatar"><div class=user-name ng-class="{\'noname\': !user.name}" title={{::user.name}}>{{ ::user.name }}</div><div class=user-email title={{user.email}}><a href=mailto:{{user.email}}>{{user.email}}</a></div></a></td><td class=users-col-id><div class=user-id title={{::user.id}}><strong>{{::user.id}}</strong></div></td><td class=users-col-status ng-class="{frozen: user.frozen_at, active: user.status==\'active\'}"><i ng-if=user.frozen_at class="fa fa-minus-circle" translate translate-attr-title=USERS.FROZEN></i> <i ng-if="user.status==\'active\'" class="fa fa-check" translate translate-attr-title={{user.status.toUpperCase()}}></i> <i ng-if="user.invitation_sent_at && !user.invitation_accepted_at" class="fa fa-envelope-o" translate translate-attr-title=INVITED></i></td><td class=users-col-level ng-if=vm.myservice.isAdmin()><div>{{user.level}} <a href ng-click="update_user(user, {level: (user.level == \'user\' ? \'manager\' : \'user\')})" ng-if="vm.myservice.isAdmin() && user.level != \'admin\'" title="{{ \'change_level_\' + user.level | uppercase | translate }}"><span><i class="fa fa-3" ng-class="\'fa-level-\' + (user.level == \'user\' ? \'up\' : \'down\')"></i></span></a></div></td><td class="users-col-station smartphone-hidden" ng-if=vm.myservice.isAdmin()><div><a ui-sref="stations.index({stationId: user.station.id})">{{::user.station.name}} ({{::user.station.id}})</a></div></td><td class=users-col-remote-status ng-repeat="app in vm.availableApps | orderBy:\'position\'"><span class="widget status"><select ng-cloak ng-change="toggleApp(user, app)" ng-model=user.services_status[app.slug] class="account-status button button-hollow {{user.services_status[app.slug]}} {{user.invitation_sent_at && !user.invitation_accepted_at || user.status!=\'active\' ? \'inactive-account\' : \'\'}}" title={{app.name.fr||app.name.en}}><option ng-disabled="shouldDisableOption(user, app, status)" ng-repeat="status in vm.statuses" value={{status}}>{{ status | uppercase | translate }}</option></select></span></td><td class=users-action><span uib-dropdown keyboard-nav=true><button id=action-button-{{user.id}} type=button class=btn uib-dropdown-toggle title=Actions><span><i class="fa fa-gear"></i></span></button><ul class="dropdown-menu dropdown-menu-left profiles" uib-dropdown-menu aria-labelledby=Actions><div class="triangle-right top"></div><li><a href user-show data-user=user><translate>USERS.SHOW</translate></a></li><li ng-if=vm.myservice.isAdmin() translate-attr-title=USERS.TRANSFER><a href station-choose=transfer choice-arguments=user><translate>USERS.TRANSFER</translate></a></li><li ng-if="vm.myservice.isAdmin() || vm.managers_can_delete"><a href ng-click=delete(user)><translate>USERS.DELETE</translate></a></li><li ng-if="(vm.myservice.isAdmin() || user.level == \'user\')"><a href ng-click="!user.frozen_at ? freeze(user) : unfreeze(user)"><translate>{{user.frozen_at ? \'USERS.UNFREEZE\' : \'USERS.FREEZE\'}}</translate></a></li><li ng-if="!user.invitation_accepted_at && !user.last_sign_in_at"><a href ng-click=reinvite(user)><translate>USERS.REINVITE</translate></a></li><li ng-if=vm.availableAppsSlugs.includes(app.slug) translate-attr-title-values="{service: app.slug}" translate-attr-title=USERS.REMOTE_REINVITE ng-repeat="app in vm.remoteApps | filter:{slug:\'munki\'} | orderBy:position"><a href ng-click="reinvite(user, app.slug)"><translate translate-values="{ service: app.name.fr||app.name.en }">USERS.REMOTE_REINVITE</translate></a></li></ul></span></td></tr></tbody></table><div class=widget-foot><div class=clearfix></div></div><div class="button more-button icon-more" ng-hide="vm.disabled || vm.fetching" ng-click=vm.nextPage()></div></div></section></section>');
$templateCache.put('app/widgets/widget-header-with-filter.html','<div class=widget-head><div class="actions pull-right"><div ng-repeat="action in actions" ng-if=action.access_levels.includes(vm.me.level) class="action button button-action" ng-click="action.fct().then(action.onsuccess, action.onerror)" translate>{{action.label}}</div></div><div class=pull-left>{{title}} <span class="badge badge-info">{{count}}</span></div><small class=page-title-subtle ng-show=subtitle>({{subtitle}})</small><div class="search pull-left page-title-subtle"><input type=search results=2 class="filter search-input" placeholder="{{\'FILTER\' | translate}}" ng-model=filter ng-model-options="{debounce: 250}"><div class="filters pull-right"><div ng-repeat="filter in filters">{{filter.query}}</div></div></div></div>');
$templateCache.put('app/widgets/widget-header.html','<div class=widget-head><div class=pull-left>{{title}}</div><small class=page-title-subtle ng-show=subtitle>({{subtitle}})</small><div class="widget-icons pull-right"></div><small class="pull-right page-title-subtle" ng-show=rightText>{{rightText}}</small><div class=clearfix></div></div>');
$templateCache.put('app/dashboard/analytics/_download.html','<div><p translate>STATS.DOWNLOAD.HINT</p><button class="button action" ng-click=$ctrl.ok() translate>DOWNLOAD</button></div>');
$templateCache.put('app/dashboard/analytics/graphset.html','<div class="panel panel-primary" ng-attr-id=graphset-{{graphSet.id}}><div class=panel-body ng-style={{graphSet.css}}><iframe class=datastudio-container width=100% height=100% data-src="{{ graphSet.builtUrl }}" frameborder=0 allowfullscreen></iframe></div></div>');
$templateCache.put('app/sign_in/partials/_denied_header.html','<div class="logo icon-divercities"></div><h2 translate>ACCESS_DENIED</h2>');
$templateCache.put('app/sign_in/partials/_signout_header.html','<div class="logo icon-divercities"></div><div class=plain-text ng-if=!isAuthed translate>DISCONNECTED_MSG</div><button class=button ui-sref=auth.signIn translate>SIGN_IN</button>');
$templateCache.put('app/sign_in/partials/_signout_side.html','<i class="menu-button fa fa-ellipsis-v"></i><script type=text/javascript>\n  $(\'i.menu-button\').click(function(e) {\n    $(\'.side-car\').addClass(\'opened\');\n    $(document).bind(\'click\', clickOutside);\n    // setTimeout(function() { $(document).bind(\'click\', clickOutside) }, 10);\n    e.preventDefault();\n    return false;\n  });\n\n  function clickOutside(e) {\n    if ($(\'.side-car\').hasClass(\'opened\') && ($(e.target).hasClass(\'nav-link\') || !$(\'.side-car\').get(0).contains(e.target))) {\n      $(\'.side-car\').removeClass(\'opened\');\n      $(document).off(\'click\', clickOutside);\n    }\n  };\n</script><side class=side-car><div></div><nav><a ui-sref=auth.invitation translate>INVITATION</a> <a ui-sref=auth.signIn translate>SIGN_IN</a> <a ui-sref=auth.signUp translate>SIGN_UP</a> <a ui-sref=auth.code translate>CODE</a> <a ui-sref=auth.pro translate>PRO</a></nav><div class=social-networks><div translate>FOLLOW_US</div><menu><a href=https://facebook.com target=_blank><i class="fa fa-facebook"></i></a> <a href=https://twitter.com target=_blank><i class="fa fa-twitter"></i></a> <a href=https://instagram.com target=_blank><i class="fa fa-instagram"></i></a> <a href=https://linkedin.com target=_blank><i class="fa fa-linkedin"></i></a></menu></div></side>');
$templateCache.put('app/sign_in/partials/_signup_mra.html','<form ng-submit=signUpCtrl.signUp() accept-charset=UTF-8 method=post><input name=utf8 type=hidden value=\u2713><h3 uppercase translate>SIGN_UP</h3><input autofocus placeholder="{{ \'EMAIL_ADDRESS\' | translate }}" type=email ng-model=signUpCtrl.email required> <input autofocus placeholder="{{ \'MRA_NUMBER\' | translate }}" type=email ng-model=signUpCtrl.mra_number required> <input placeholder="{{ \'MRA_PASSWORD\' | translate }}" type=password ng-model=signUpCtrl.password required><div class=info translate>MRA_PASSWORD_HINT</div><a ui-sref="auth.signUpPassword({provider:\'mra\'})" translate>MRA_PASSWORD_FORGOTTEN</a><div class=submit-field><input type=submit name=commit value="{{ \'CONTINUE\' | translate }}" class=button></div><p>Une question ? <a href=mailto:hello@1dtouch.com>Contactez-nous</a> !<br>D\xE9j\xE0 inscrit ? <a ui-sref=auth.signIn translate>SIGN_IN</a> !</p></form>');
$templateCache.put('app/sign_in/partials/_signup_mra_forgotpassword.html','<form ng-submit=signUpCtrl.retrievePassword() accept-charset=UTF-8 method=post><input name=utf8 type=hidden value=\u2713><div class=info uppercase translate>RETRIEVE_MRA_PASSWORD_HINT</div><input autofocus placeholder="{{ \'LAST_NAME\' | translate }}" type=email ng-model=signUpCtrl.last_name required> <input autofocus placeholder="{{ \'MRA_NUMBER\' | translate }}" type=email ng-model=signUpCtrl.mra_number required><div class=submit-field><input type=submit name=commit value="{{ \'SEND\' | translate }}" class=button></div><a ui-sref=auth.signUp translate>BACK_TO_SIGN_UP_FORM</a></form>');
$templateCache.put('app/sign_in/partials/_signup_mra_forgotpassword_header.html','<div class=logo><img src=/images/logo.png alt=Logo> <img src=/images/client_logos/logo_mra.png alt="Logo mra"></div><h2 translate>FORGOTTEN_PASSWORD</h2>');
$templateCache.put('app/sign_in/partials/_signup_mra_header.html','<div class=logo><div class=icon-divercities></div><img src=/images/client_logos/logo_mra.png alt="Logo mra"></div><h2 translate>LETS_SIGNUP</h2>');
$templateCache.put('app/sign_in/partials/_worlds_footer.html','<div id=worlds-switcher><div class=wrapper></div><div></div></div>');}]);
angular.module('ngHeatmap', [])
  .directive('ngHeatmap', ['$http', function($http) {
    return {
      link: function($scope, $elem, $attr) {
        $http.get($attr.url).then(function(response) {
          var json = response.data;
          var start = new Date($attr.start);
          var start_date = new Date(start.getTime() - 3600 * 24 * 1000);
          var end = new Date(start_date.getFullYear() + 1, start_date.getMonth(), start_date.getDate());
          var end_date = new Date(end.getTime() + 3600 * 48 * 1000);

          var width = $elem.width();
          var cellSize = $attr.cellsize || 15;
          var height = cellSize * 7 + 60;


          var day = d3.time.format('%w'),
              week = d3.time.format('%U'),
              format = d3.time.format('%Y-%m-%d');

          var start_week = parseInt(week(start_date)),
              last_week_of_first_year = parseInt(week(new Date(start_date.getFullYear(), 11, 31))),
              number_of_weeks_in_first_year = last_week_of_first_year - start_week;

          var relative_week = function(date) {
            if (date.getFullYear() == start_date.getFullYear()) {
              return (week(date) - start_week);
            } else {
              return (parseInt(week(date)) + number_of_weeks_in_first_year);
            }
          };

          var g_offset_top = 30;

          var svg = d3.select('.' + $attr.class)
            .append('svg')
            .attr('width', width)
            .attr('height', height)
          .append('g')
            .attr('transform', 'translate(0,' + g_offset_top + ')');

          var tooltip = d3.select('.' + $attr.class)
            .append('div')
            .attr('class', 'hm-tooltip');

          var rect = svg.selectAll('.day')
            .data(function() { return d3.time.days(start_date, end_date); })
            .enter().append('rect')
            .attr('class', 'day tooltip-item')
            .attr('width', cellSize)
            .attr('height', cellSize)
            .attr('x', function(d) { return relative_week(d) * cellSize; })
            .attr('y', function(d) { return day(d) * cellSize; })
            .datum(format)
            .on('mouseover', function() {
              var text = d3.select(this).select('title').text();

              tooltip.text(text);

              var tooltip_position_left = parseInt(d3.select(this).attr('x')),
                  tooltip_position_top = parseInt(d3.select(this).attr('y'));

              tooltip_position_top += g_offset_top;

              var tooltip_width = angular.element(tooltip[0][0]).width(),
                  tooltip_height = angular.element(tooltip[0][0]).height();

              tooltip_position_left -= tooltip_width / 2;
              tooltip_position_top -= (tooltip_height * 2);

              tooltip
                .style('display', 'block')
                .style('top', tooltip_position_top + 'px')
                .style('left', tooltip_position_left + 'px')
            })
            .on('mouseout', function() {
              tooltip.style('display', 'none');
            });

          var values = [];
          for (var key in json) {
            values.push(json[key]);
          }

          if (values.length > 0) {
            var max = Math.max.apply(Math, values);
          } else {
            var max = 0;
          }

          var color = d3.scale.quantize()
            .domain([0, max])
            .range(d3.range(5).map(function(d) { return 'q' + d; }));

          rect.append('title')
            .text(function(d) { return 'Aucune écoute le ' + moment(d).format('LL'); });

          rect.filter(function(d) { return d in json; })
            .attr('class', function(d) { return 'day ' + color(json[d]); })
            .select('title')
            .text(function(d) { return json[d] + ' écoutes le ' + moment(d).format('LL'); });


          var label_start = moment(start_date).subtract(1, 'month');
          var label_end = moment(end_date).subtract(1, 'month');

          svg.selectAll('.month-label')
            .data(function() { return d3.time.months(label_start, label_end); })
            .enter().append('text')
            .text(function(d) {
              return moment(d).format('MMM');
            })
            .attr('x', function(d) {
              return Math.max(0, (relative_week(new Date(d)) + 1) * cellSize);
            })
            .attr('y', -10)
            .attr('class', 'month-label');

          var legend = d3.select('.' + $attr.class + ' svg')
            .append('g')
            .attr('class', 'legend-container')
            .attr('transform', 'translate(0,' + (height - 20) + ')');

          legend.selectAll('.legend')
            .data([0, 1, 2, 3, 4, 5])
            .enter().append('rect')
            .attr('class', function(d) {
              return 'day ' + 'q' + (d - 1);
            })
            .attr('width', cellSize)
            .attr('height', cellSize)
            .attr('x', function(d) { return d * cellSize + 10; })
            .attr('y', 0);

          legend.selectAll('legend-label-min')
            .data([0])
            .enter().append('text')
            .attr('class', 'legend-label-min')
            .text(function(d) { return d; })
            .attr('y', 12)
            .attr('x', 0);

          legend.selectAll('legend-label-max')
            .data([max])
            .enter().append('text')
            .attr('class', 'legend-label-max')
            .text(function(d) { return d; })
            .attr('y', 12)
            .attr('x', 6 * cellSize + 15);
        });
      }
    };
  }]);

(function () {
    'use strict';

    var core = angular
        .module('app.users')
        .directive('userClean', ['$q', '$stateParams', 'dataservice', 'logger', '$uibModal', 'myservice', 'userservice', function($q, $stateParams, dataservice, logger, $uibModal, myservice, userservice) {
          return {
            restrict: 'A',
            scope: true,
            link: function(scope, element, attrs) {
                $(element).click(function() {
                    var deferred = $q.defer();
                    console.log('user-clean', scope.station);

                    var $uibModalInstance = $uibModal.open({
                        animation: true,
                        ariaLabelledBy: 'modal-title',
                        ariaDescribedBy: 'modal-body',
                        templateUrl: 'app/users/_freeze_inactive.html',
                        controller: function() {
                            var $ctrl = this;
                            $ctrl.monthsAgo = 3;
                            $ctrl.myservice = myservice;
                            var stationId = $stateParams.station_id || (myservice.isAdmin() ? null : myservice.me().station.id);
                            $ctrl.stationId = stationId;

                            $ctrl.ok = function () {
                                if ($ctrl.monthsAgo) {
                                    $uibModalInstance.close($ctrl);
                                }
                            };

                            $ctrl.cancel = function () {
                                $uibModalInstance.dismiss('cancel');
                            };
                        },
                        controllerAs: '$ctrl',
                        size: 'sm'
                    });

                    $uibModalInstance.result.then(function(params) {
                        userservice.freezeAll({station_id: params.stationId, months_ago: params.monthsAgo}).then(function(response) {
                            // logger.success('OK :)');
                            if (scope.onsuccess) { scope.onsuccess(response); };
                        });
                        console.log('cleaning', params.monthsAgo, params.stationId);
                    }, function(reason) {
                        if (scope.onerror) { scope.onerror(reason); };
                        console.log('dismiss', reason);
                    });
                    
                    return deferred.promise;
                });
            }
          };
        }]);
})();
(function () {
    'use strict';

    var core = angular
        .module('app.users')
        .directive('userExport', ['$q', '$stateParams', 'dataservice', 'logger', '$uibModal', 'myservice', 'userservice', '$translate', 'DataProcessingService', function($q, $stateParams, dataservice, logger, $uibModal, myservice, userservice, $translate, DataProcessingService) {
          return {
            restrict: 'A',
            scope: true,
            link: function(scope, element, attrs) {
                $(element).click(function() {
                  var deferred = $q.defer();
                  var blobUrl;
                  console.log('export users');

                  var stationId = myservice.isAdmin() ? ($stateParams.station_id || $stateParams.station) : (dataservice.station.id || myservice.me().station.id);
                  if (!stationId) { logger.warning('Too much users :/'); return; }

                  var $uibModalInstance = $uibModal.open({
                    animation: true,
                    ariaLabelledBy: 'modal-title',
                    ariaDescribedBy: 'modal-body',
                    templateUrl: 'app/dashboard/analytics/_download.html',
                    controller: function() {
                      var $ctrl = this;
                      $ctrl.ok = function() {
                        $uibModalInstance.close(true);
                      };
                    },
                    controllerAs: '$ctrl',
                    size: 'md'
                  });
                  $uibModalInstance.result.then(function(ok) {
                    console.log('exporting data');
                    $translate('STATS.EXPORTING').then(function(txt) {
                      logger.info(txt);
                    });
                    // get data, convert and save
                    var blobUrl;
                    userservice.getUsers(scope.vm.filter, {params: {per_page: 3000}, station_id: stationId}).then(function(data) {
                      // convert to CSV and save
                      var csv = DataProcessingService.convert(data.items, 'csv', {columns: ['type', 'id', 'name', 'email', 'level', 'status', 'access', 'station_id', 'uid', 'created_at', 'expires_at', 'creative_fields', 'station']});
                      var blob = new Blob([csv], {encoding: 'UTF-8', type: 'text/csv; charset=UTF-8'});
                      if (blobUrl !== null) {
                        window.URL.revokeObjectURL(blobUrl);
                      }
                      blobUrl = window.URL.createObjectURL(blob);

                      var a = $("<a style='display: none;'/>");
                      a.attr('href', blobUrl);
                      a.attr('download', 'users.csv');
                      $('#users-view').append(a);
                      a[0].click();
                      a.remove();
                      // returns a URL you can use as a href
                      // $window.location.href = blobUrl;
                      logger.success('OK :)');
                  });
                  }, function () {
                    //
                  });

                  return deferred.promise;
                });
            }
          };
        }]);
})();
(function () {
    'use strict';

    var core = angular
        .module('app.users')
        .directive('userInviteMultiple', ['$q', '$stateParams', 'dataservice', 'logger', '$uibModal', 'myservice', 'userservice', '$translate', function($q, $stateParams, dataservice, logger, $uibModal, myservice, userservice, $translate) {
          return {
            restrict: 'A',
            scope: true,
            link: function(scope, element, attrs) {
                $(element).click(function() {
                  var deferred = $q.defer();
                  console.log('user-invite-multiple', scope.station);

                  var $uibModalInstance = $uibModal.open({
                    animation: true,
                    ariaLabelledBy: 'modal-title',
                    ariaDescribedBy: 'modal-body',
                    templateUrl: 'app/users/_invite_multiple.html',
                    controller: ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
                      var $ctrl = this;
                      $ctrl.invitation = {station_invitation_message: ''};
                      $ctrl.myservice = myservice;
                      $ctrl.station_id = parseInt($stateParams.station || myservice.me().station.id);

                      $ctrl.ok = function () {
                        if ($ctrl.invitation.station_invitation_message) {
                          var params = {
                            station_id: $ctrl.station_id,
                            station: {
                              invitation_message:     $ctrl.invitation.station_invitation_message,
                              csv_file:               $ctrl.invitation.station_csv_file
                            }
                          };
                          console.log('invite multiple', params);
                          $uibModalInstance.close(params);
                        }
                      };

                      $ctrl.cancel = function () {
                        $uibModalInstance.dismiss('cancel');
                      };

                      if ($stateParams.station_id || myservice.me().station.id) {
                        dataservice.getStation($stateParams.station_id || myservice.me().station.id).then(function(station) {
                          if (station) {
                            $translate("STATIONS_INVITATION_MESSAGE", {station: station}).then(function(invitation_message) {
                                $ctrl.invitation.station_invitation_message = invitation_message;
                            });
                          }
                        });
                      }
                    }],
                    controllerAs: '$ctrl',
                    size: 'md'
                  });
                  $uibModalInstance.result.then(function(data) {
                    data.station.invitation_message = $(document).find('.invite-multiple .redactor').redactor('code.get');
                    userservice.inviteMultiple(data).then(function(response) {
                      logger.success('OK :)', params);
                      refresh();
                    });
                    console.log('invite sending', data);
                  }, function(reason) {
                    console.log('dismiss', reason);
                  });

                  return deferred.promise;
                });
            }
          };
        }]);
})();
(function () {
    'use strict';

    var core = angular
        .module('app.users')
        .directive('userInviteOne', ['$q', '$stateParams', 'dataservice', 'logger', '$uibModal', 'myservice', 'userservice', function($q, $stateParams, dataservice, logger, $uibModal, myservice, userservice) {
          return {
            restrict: 'A',
            scope: true,
            link: function(scope, element, attrs) {
                $(element).click(function() {
                    var deferred = $q.defer();
                    console.log('user-invite-one', scope);

                  var $uibModalInstance = $uibModal.open({
                    animation: true,
                    ariaLabelledBy: 'modal-title',
                    ariaDescribedBy: 'modal-body',
                    templateUrl: 'app/users/_invite_one.html',
                    resolve: {
                      station: function() {
                        return dataservice.getStation($stateParams.station || myservice.me().station.id);
                      }
                    },
                    controller: ['$scope', 'station', function($scope, station) {
                      var $ctrl = this;
                      $ctrl.vm = scope.vm;
                      $ctrl.sending = false;
                      $ctrl.myservice = myservice;

                      $ctrl.user = {};
                      if (!station) {
                        $uibModalInstance.dismiss('cancel');
                        console.log('invite', scope, $ctrl, station, $scope);
                      }
                      $ctrl.station = station;
                      $ctrl.user.station_id = station.id;

                      $ctrl.availableApps = scope.vm.availableApps.filter(function(v) {return v.slug!='munki' && station.settings.default_creative_fields.indexOf(v.slug) > -1;});
                      $ctrl.creative_fields = scope.vm.availableApps.reduce(function(acc,v) { acc.concat(v.slug); return acc; }, []);
                      $ctrl.munki = scope.vm.munki;
                      console.log('modal scope', $scope, '$ctrl', $ctrl, 'station', station);

                      $ctrl.ok = function () {
                        if (!$ctrl.myservice.isAdmin()) {
                          $ctrl.user.level = 'user';
                        }
                        console.log($ctrl.user);
                        if ($ctrl.user.level && $ctrl.user.name && $ctrl.user.email) {
                          $uibModalInstance.close($ctrl.user);
                        }
                      };

                      $ctrl.cancel = function () {
                        $uibModalInstance.dismiss('cancel');
                      };
                    }],
                    controllerAs: '$ctrl',
                    size: 'sm'
                  });
                  $uibModalInstance.result.then(function(data) {
                    data.creative_fields ||= [];
                    if (data.creative_field_munki) {
                      delete data.creative_field_munki;
                      data.creative_fields.push('munki');
                    }
                    var user = $.extend({}, data);
                    
                    userservice.inviteOne(user).then(function(response) {
                      console.log('invite sent', user);
                      if (scope.onsuccess) {scope.onsuccess.call();} else {logger.success('OK :)', user)};
                    }, function(response) {
                      console.log(response);
                      logger.error(response.data.description);
                      logger.warning(response.data.description);
                    });
                  }, function(reason) {
                    console.log('dismiss', reason);
                  });

                  return deferred.promise;
                });
            }
          };
        }]);
})();

(function () {
    'use strict';

    var core = angular
        .module('app.users')
        .directive('userReinviteAll', ['$q', '$stateParams', 'dataservice', 'logger', '$uibModal', 'myservice', 'userservice', function($q, $stateParams, dataservice, logger, $uibModal, myservice, userservice) {
          return {
            restrict: 'A',
            scope: true,
            link: function(scope, element, attrs) {
                $(element).click(function() {
                  var deferred = $q.defer();
                  console.log('user-reinvite-all', scope.station);

                  var $uibModalInstance = $uibModal.open({
                    animation: true,
                    ariaLabelledBy: 'modal-title',
                    ariaDescribedBy: 'modal-body',
                    templateUrl: 'app/users/_reinvite.html',
                    controller: function() {
                      var $ctrl = this;
                      var stationId = $stateParams.station_id || myservice.me().station.id;
                      if (dataservice.station.id != stationId) {
                        dataservice.getStation($stateParams.station || myservice.me().station.id).then(function(station) {
                          if (station) {
                            $ctrl.station = station;
                          }
                        });
                      } else {
                        $ctrl.station = dataservice.station;
                      }

                      $ctrl.ok = function () {
                        if ($ctrl.station.id) {
                          $uibModalInstance.close($ctrl.station.id);
                        }
                      };

                      $ctrl.cancel = function () {
                        $uibModalInstance.dismiss('cancel');
                      };
                    },
                    controllerAs: '$ctrl',
                    size: 'sm'
                  });

                  $uibModalInstance.result.then(function(stationId) {
                    userservice.reinviteAll({station_id: stationId}).then(function(data) {
                      logger.success('OK :)');
                    });
                    console.log('re-invite sending', stationId);
                  }, function(reason) {
                    console.log('dismiss', reason);
                  });

                  return deferred.promise;
                });
            }
          };
        }]);
})();
(function () {
  'use strict';

  var core = angular
    .module('app.users')
    .directive('userShow', ['$q', '$localStorage', '$stateParams', 'userservice', 'logger', '$uibModal', 'myservice', 'MainService', 
      function($q, $localStorage, $stateParams, userservice, logger, $uibModal, myservice, MainService) {
      return {
        restrict: 'A',
        scope: {
          user: '='
        },
        link: function(scope, element, attrs) {
          $(element).click(function() {
            var parentElem = scope.parentSelector ? 
              angular.element($document[0].querySelector('.modal-user ' + scope.parentSelector)) : undefined;

            var $uibModalInstance = $uibModal.open({
              animation: true,
              ariaLabelledBy: 'modal-title',
              ariaDescribedBy: 'modal-body',
              templateUrl: 'app/users/_user.html',
              controller: ['$scope', 'user', function($scope, user) {
                var $ctrl = this;
                $ctrl.isAdmin = myservice.isAdmin();
                $ctrl.user = user;
                $ctrl.station = myservice.me().station;
                $ctrl.user.expires_at = $ctrl.user.expires_at ? new Date($ctrl.user.expires_at) : null;
                $ctrl.user.created_at = $ctrl.user.created_at ? new Date($ctrl.user.created_at) : null;
                $ctrl.user.updated_at = $ctrl.user.updated_at ? new Date($ctrl.user.updated_at) : null;
                $ctrl.user.last_sign_in_at = $ctrl.user.last_sign_in_at ? new Date($ctrl.user.last_sign_in_at) : null;
                $ctrl.user.renewed_at = $ctrl.user.renewed_at ? new Date($ctrl.user.renewed_at) : null;
                $ctrl.fetching = false;
                $ctrl.codes = [];
                $ctrl.params = { };
                $ctrl.apps = myservice.isAdmin() ? MainService.appsSlug() : user.station && user.station.creative_fields;
                $ctrl.filter_qry = '';
                $scope.$watch('$ctrl.filter_qry', function(val, old_val) {
                  if (val != old_val) {
                    $ctrl.filter({query: val});
                  }
                });
                // console.log(user, $ctrl);


                $ctrl.ok = function (user) {
                  $uibModalInstance.close(user);
                };
                $ctrl.renew = function () {
                  console.log('renewing account', $ctrl.user);
                  $uibModalInstance.close();
                  $scope.sending = true;
                  var params = { 
                    station_id: $ctrl.user.station_id, 
                    id: $ctrl.user.id, 
                  };
                  userservice.renew(params).then(function(response) {
                      logger.success('OK :)', $ctrl.user)
                    }, function(response) {
                      console.log(response)
                      logger.error('Error : ' + response.message)
                    })
                    .finally(function() {
                      $scope.sending = false;
                    })
                };
                $ctrl.update = function () {
                  $uibModalInstance.close();
                  $scope.sending = true;
                  var params = { 
                    station_id: $ctrl.user.station_id, 
                    id: $ctrl.user.id, 
                    name: $ctrl.user.name,
                    email: $ctrl.user.email
                  };
                  userservice.update(params).then(function(response) {
                      logger.success('OK :)', $ctrl.user)
                    }, function(response) {
                      console.log(response)
                      logger.error('Error : ' + response.message)
                    })
                    .finally(function() {
                      $scope.sending = false;
                    })
                };
                $ctrl.update_validity = function () {
                  $uibModalInstance.close();
                  $scope.sending = true;
                  var params = { 
                    station_id: $ctrl.user.station_id, 
                    id: $ctrl.user.id, 
                    expire_at: $ctrl.user.expires_at.toISOString()
                  };
                  userservice.update(params).then(function(response) {
                      logger.success('OK :)', $ctrl.user)
                    }, function(response) {
                      console.log(response)
                      logger.error('Error : ' + response.message)
                    })
                    .finally(function() {
                      $scope.sending = false;
                    })
                };
                $ctrl.update_settings = function () {
                  $uibModalInstance.close();
                  $scope.sending = true;
                  var params = { 
                    id: $ctrl.user.id, 
                    settings: $ctrl.user.settings
                  };
                  userservice.update(params).then(function(response) {
                      logger.success('OK :)', $ctrl.user)
                    }, function(response) {
                      console.log(response)
                      logger.error('Error : ' + response.message)
                    })
                    .finally(function() {
                      $scope.sending = false;
                    })
                };


                $ctrl.filter = function(params) {
                  $ctrl.params = $.extend($ctrl.params, params);
                  if (!params.query) { $ctrl.filter_qry = ''; }
                  $ctrl.getOffers($ctrl.params);
                };
                $ctrl.getOffers = function (params) {
                  params = $.extend(params, {
                    id: $ctrl.user.id,
                    station_id: $ctrl.user.station.id
                  });
                  userservice.offers(params).then(function(data) {
                    $ctrl.codes = data;
                    $ctrl.meta = data.meta;
                    $ctrl.fetching = false;
                  });
                };
                $ctrl.cancel = function () {
                  $uibModalInstance.dismiss('cancel');
                };
                $ctrl.heatmapUrl = MainService.apiEndpoint() + 'users/' + user.id + '/events';
                $ctrl.startDate = moment().subtract(12, 'month').toISOString();
              }],
              controllerAs: '$ctrl',
              resolve: {
                user: function() {
                  return userservice.getUser(scope.user);
                },
                services: function(MainService) {
                  return MainService.fetchApps();
                }
              },
              size: 'lg',
              appendTo: parentElem
            });
          });
        }
      };
    }]);
})();
(function () {
    "use strict";

    angular
        .module('app.users')
        .controller('UsersController', UsersController);

    UsersController.$inject = ['$scope', '$q', '$timeout', '$localStorage', '$document', '$state', '$stateParams', 'dataservice', 'userservice', 'myservice', 'MainService', 'logger', '$location', '$uibModal', '$translate', 'station'];
    /*  */
    function UsersController($scope, $q, $timeout, $localStorage, $document, $state, $stateParams, dataservice, userservice, myservice, MainService, logger, $location, $uibModal, $translate, station) {
        var vm = this;
        var blobUrl;
        var usersPromise;

        vm.news = {
            title: 'Kiosque 1D lab',
            description: 'Manage your community, browse your stats.'
        };


        if (!myservice.isAuthed() || !['manager', 'admin'].includes(myservice.me() && myservice.me().level)) {
          myservice.signOut();
          $localStorage.redirectAfterSignInPath = 'users.index';
          $state.go('auth.signIn');
        }

        $scope.station = station || '?';
        $scope.$state = $state;


        $translate('USERS.DESCRIPTION').then(function(txt) {
          vm.news.description = txt;
        });
        vm.SCHEME = SCHEME;
        vm.APP = window.location.hostname.split('.')[0].replace(/^terminal-/, '');
        vm.DC_DOMAIN = DC_DOMAIN;
        vm.userCount = '?';
        MainService.fetchApps().then(function(data) {
          vm.apps = data;
          if (myservice.isAdmin()) {
            vm.availableApps = vm.apps.filter(function(app) { return app.available; });
          } else {
            var stationApps = station && station.creative_fields;
            vm.availableApps = vm.apps.filter(function(app) { return stationApps.includes(app.slug) })
          }
          vm.availableAppsSlugs = vm.availableApps.map(function(app) { return app.slug; });
          vm.munki = vm.availableApps.some(function(service) { return service.slug == 'munki'; });
          vm.remoteApps = vm.apps.filter(function(app) { return app.remote_service })
        }, function(reason) {
          console.log('fetchApps error', reason);
          myservice.signOut();
          $localStorage.redirectAfterSignInPath = 'users.index';
          $state.go('auth.signIn');
        });

        vm.users = {items: []};
        $translate('USERS').then(function(txt) {
          vm.title = txt;
        });
        $translate('USERS.SUBTITLE').then(function(txt) {
          vm.subtitle = txt;
        });

        vm.page = 0;
        vm.fetched = false;
        vm.disabled = false;
        vm.stateParams = $stateParams;
        vm.filter = $stateParams.filter;
        vm.nextPage = nextPage;
        vm.myservice = myservice;
        vm.dataservice = dataservice;
        vm.usersTotals = {};
        vm.sortParams = { sort: 'status', order: 'desc' };
        var params = { page: vm.page, sort: vm.sortParams.sort, order: vm.sortParams.order };
        vm.managers_can_delete = station && station.settings.managers_can_soft_delete_users;
        // Set today date to compare expire date
        $scope.now = new Date();
        $scope.now.toISOString();
        $scope.$watch('vm.sortParams', function(v,o) {
          if (myservice.isAuthed() && myservice.me() && !myservice.me().id && !activated) { return; }
          if (!v || (v.sort==o.sort && v.order==o.order)) { return; }
          params.sort = v.sort;
          params.order = v.order;
          console.log('sort params: sorting', params);
          refresh();
        });

        // stations create form callbacks
        $scope.onsuccess = function(user, message) { $scope.refresh(); logger.success('OK :)'); };
        $scope.onerror = function(user, message) { $scope.refresh(); logger.error("La station n'a pas été créée :/"); };

        // init
        var activated = false;
        $scope.$watch(function() {
          return myservice.isAuthed() && myservice.me();          
        }, function(v, o) {
          if (v && v.id) {
            if (!activated) {
              if ($scope.station) {params.station_id = $scope.station.id;}
              for (var param in $stateParams) {
                  if (param && param!='id') {
                      params[param] = $stateParams[param];
                  }
              }
              $scope.selectedFilter = '';
              if ($stateParams.status) {
                $scope.selectedFilter = $stateParams.status;
              }
              if ($stateParams.filter) {
                $scope.selectedFilter = $stateParams.filter;
              }
              if ($stateParams.level) {
                $scope.selectedFilter = $stateParams.level;
              }
              if ($stateParams.app) {
                $scope.selectedFilter = $stateParams.app;
              }

              // console.log('current user', v, params, $scope.selectedFilter);
              activate();
            }
          }
        });

        function search(ev) {
          if (ev && (ev.keyCode !== 13)) return;
          vm.page = 0;
          vm.disabled = false;
          vm.users = {items:[], meta: 0};
          vm.filter = vm.searchString;
          refresh();
        }
        $scope.search = search;


        function activate() {
          activated = true;
          if ($scope.station) {params.station_id = $scope.station.id;}
          vm.statuses = ['-','active'];
          if (myservice.isAdmin()) vm.statuses.push('expired');
          refresh();
        } // activate



        // UPDATE
        $scope.update_user = function(user, attrs) {
            var params = $.extend({ id: user.id, station_id: user.station_id} , attrs);
            userservice.update(params)
            .then(function(data) {
                logger.success('Ok :)', data);
                var idx = vm.users.items.find(function(item) { return user.id == item.id; });
                $scope.$applyAsync(function() {
                  vm.users.items[idx] = $scope.setServiceStatus(data);
                });
            });
        }
        $scope.transfer = function(args, station) {
          userservice.transfer(args, station).then(function(response) {
            logger.success('OK :)', response);
            var idx = vm.users.items.find(function(item) { return args.id == item.id; });
            $scope.$applyAsync(function() {
              vm.users.items.splice(idx, 1);
            });
            fetchTotals();
          });
        };
        $scope.delete = function(args) {
          console.log('delete', args);
          $scope.sending = true;
          userservice.delete(args).then(function(response) {
            logger.success('OK :)', response);
            $scope.sending = false;
            var idx = vm.users.items.find(function(item) { return args.id == item.id; });
            $scope.$applyAsync(function() {
                vm.users.items.splice(idx, 1);
            },
            function(response) {
              logger.error('Erreur :(');
            });
            fetchTotals();
          });
        };
        // FREEZE / UNFREEZE
        $scope.freeze = function(args, service) {
          console.log('freeze', args);
          $scope.sending = true;
          userservice.freeze(args, service).then(function(response) {
            logger.success('OK :)', response);
            $scope.sending = false;
            var idx = vm.users.items.find(function(item) { return args.id == item.id; });
            $scope.$applyAsync(function() {
                vm.users.items[idx] = $scope.setServiceStatus(response);
            },
            function(response) {
              logger.error('Erreur :(');
            });
            fetchTotals();
          });
        };
        $scope.unfreeze = function(args, service) {
          console.log('unfreeze', args);
          $scope.sending = true;
          userservice.unfreeze(args, service).then(function(response) {
            logger.success('OK :)', response);
            $scope.sending = false;
            var idx = vm.users.items.find(function(item) { return args.id == item.id; });
            $scope.$applyAsync(function() {
                vm.users.items[idx] = $scope.setServiceStatus(response);
            },
            function(response) {
              logger.error('Erreur :(');
            });
            fetchTotals();
          });
        };
        $scope.reinvite = function(args, service) {
          console.log('reinvite', args);
          if (!!args.email) {
            $scope.sending = true;
            userservice.reinvite(args, service).then(function(response) {
              logger.success('OK :)', response);
              $scope.sending = false;
            });
          }
        }
        $scope.onSelectedChange = function() {
          var filter_params = {};
          switch ($scope.selectedFilter) {
            case 'manager':
            case 'user':
              filter_params = {level: $scope.selectedFilter};
              break;
            case 'active':
            case 'frozen':
            case 'invited':
              filter_params = {status: $scope.selectedFilter};
              break;
          }
          vm.availableApps.forEach(function(app) {
            if (app.slug==$scope.selectedFilter) {
              filter_params = {app: app.slug};
            }
          });
          if ($location.search().station && myservice.isAdmin()) {
            filter_params.station = $location.search().station;
          }
          // console.log('onSelectedChange', vm, $scope, filter_params);
          $state.go('users.index', filter_params, {inherit: false});
        }
        // TOGGLE AVAILABLE APPS
        $scope.toggleApp = function(user, app) {
          var station_scope = this.$parent;
          var creative_fields = [];
          if (!app.available) {
            $translate('CREATIVE_FIELD.UNACTIVABLE').then(function(txt) {logger.info(txt)});
          } else if (user.station.creative_fields.includes(app.slug) || user.creative_fields.includes(app.slug)) {
            angular.copy(user.creative_fields, creative_fields);
            if (creative_fields.includes(app.slug)) {
                var idx = creative_fields.indexOf(app.slug);
                creative_fields.splice(idx, 1);
            } else {
                creative_fields.push(app.slug);
            }

            var params = { id: user.id, creative_fields: creative_fields };
            userservice.apps(params).then(function(data) {
              console.log('toggleApp', data);
              logger.success('Utilisateur mis à jour :)', data);
              var idx = vm.users.items.find(function(item) { return data.id == item.id; });
              station_scope.$applyAsync(function() {
                  vm.users.items[idx] = $scope.setServiceStatus(data);
                  station_scope.user = $scope.setServiceStatus(data);
              });
              // refresh();
              fetchTotals();
            }, function(response) {
              console.log('toggleApp failed', response);
              logger.error(response.data.description);
              logger.warning(response.data.description);
            });
          } else {
            $translate('CREATIVE_FIELD_MISSING').then(function(txt) {logger.error(txt)});
          }
        };

        $scope.setServiceStatus = function(user) {
          user.services_status = {};
          angular.forEach(user.station.creative_field_accesses, function(service) {
            user.services_status[service.creative_field_slug] = (user.creative_field_accesses[service.creative_field_slug] && user.creative_field_accesses[service.creative_field_slug].access && (!user.creative_field_accesses[service.creative_field_slug].remote || user.creative_field_accesses[service.creative_field_slug].account_id)) ? 'active' : (user.creative_field_accesses[service.creative_field_slug] && myservice.isAdmin() && user.creative_field_accesses[service.creative_field_slug].expired_at ? 'expired' : '-');
          });
          return user;
        }
        $scope.shouldDisableOption = function(user, app, status) {
          return !myservice.isAdmin() && ['invited', 'expired'].includes(status);
        };

        function getUserTotals(config) {
          var params = {params: {count: '✓'}};
          if ($location.search().station && myservice.isAdmin()) {
            params.station_id = $location.search().station;
          } else {
            params.station_id = myservice.me().station.id;
          }
          params = $.extend(params, config);
          return userservice.getUsers(null, params);
        }

        function getUsers(filter, config) {
          // if (vm.disabled === filter) { return; }
         
          if (!config.params.page) { config.params.page = vm.page; }
          if (myservice.isAdmin()) { // Admin
            if ($location.search().station) {
              config.station_id = $location.search().station;
            }
          } else { // Not Admin
            if (myservice.me().station.id) {
              config.station_id = myservice.me().station.id;
            } else  {
              var deferred = $q.defer();
              deferred.reject('not authorized');
              return deferred.promise.catch(angular.noop);
            }
          }
          return userservice.getUsers(filter, config);
        }

        function nextPage() {
          if (vm.disabled === vm.filter || !myservice.isAuthed() || myservice.me() && !myservice.me().id || vm.fetched && vm.fetched===vm.filter) { return; }
          vm.fetched = vm.filter;
          vm.page++;
          params.page = vm.page;
          return getUsers(vm.filter, {params: params})
          .then(function (data) {
            vm.users.items = vm.users.items.concat(data.items.map($scope.setServiceStatus));
            vm.users.meta = data.meta;
            if (data.meta.total === vm.users.items.length) {
              vm.disabled = vm.filter; // Disable further calls if there are no more items
            }
            vm.fetched = vm.filter;
            // console.log('next page', vm);
            return vm.users;
          });
        }

        function refresh() {
          // console.log('refresh', params, vm, $scope);
          params.page = vm.page = 1;
          fetchTotals();
          vm.users.items = [];
          if (usersPromise) { usersPromise.cancel(); usersPromise = null; }
          usersPromise = getUsers(vm.filter, {params: params})
          .then(function (data) {
            if (!usersPromise) { return; }
            vm.users.items = data.items.map($scope.setServiceStatus);
            vm.users.meta = data.meta;
            if (data.meta.total === vm.users.items.length) {
              vm.disabled = vm.filter; // Disable further calls if there are no more items
            }
            usersPromise = null;
            vm.fetched = vm.filter;
            $scope.$applyAsync(function() {
              $timeout(function() { $('body').scrollTop(0); }, 500);
            });
            return vm.users;
          });
        }

        function fetchTotals() {
          getUserTotals().then(function (data) {
            vm.usersTotals = data.meta;
            return vm.usersTotals;
          });
        }
        $scope.refresh = refresh;
    }
})();

(function() {
    'use strict';

    angular
        .module('app.users')
        .run(appRun);

    appRun.$inject = ['routerHelper'];
    /*  */
    function appRun(routerHelper) {
        routerHelper.configureStates(getStates());

        function getStates() {
            return [
                {
                    state: 'users',
                    config: {
                        url: '/users',
                        // controller: 'UsersController',
                        // controllerAs: 'vm',
                        title: 'Users',
                        abstract: true
                    }
                },
                {
                    state: 'users.index',
                    config: {
                        url: '?station&app&level&status&filter',
                        // reloadOnSearch: false,
                        views: {
                            '@': {
                                templateUrl: 'app/users/users.html',
                                controller: 'UsersController',
                                controllerAs: 'vm',
                            }
                        },
                        title: 'users',
                        settings: {
                            nav: 3,
                            level: ['manager', 'admin'],
                            content: '&#9;<span class="smartphone-hidden">Vos utilisateurs</span>'
                        },
                        resolve: {
                            station: ['$stateParams', 'dataservice', 'myservice', function($stateParams, dataservice, myservice) {
                                var station = null;
                                if (myservice.me()) {

                                    if ($stateParams.station && myservice.isAdmin()) {
                                        station = dataservice.getStation($stateParams.station);
                                    } else {
                                        station = myservice.me().station;
                                    }
                                }
                                return station;
                            }]
                        }
                    }
                }
            ];
        }
    }
})();

(function () {
    'use strict';

    angular
        .module('app.users')
        .factory('userservice', userservice);

    userservice.$inject = ['$http', '$q', 'ItemsApi', 'exception', 'logger', 'myservice', 'MainService', 'uploadApi'];
    /*  */
    function userservice($http, $q, ItemsApi, exception, logger, myservice, MainService, uploadApi) {
        var service = {
            getUser: getUser,
            renew: renew,
            getUsers: getUsers,
            getUserRemoteStatus: getUserRemoteStatus,
            getUserCount: getUserCount,
            update: update,
            apps: apps,
            offers: offers,
            inviteOne: inviteOne,
            inviteMultiple: inviteMultiple,
            reinviteAll: reinviteAll,
            reinvite: reinvite,
            freezeAll: freezeAll,
            freeze: freeze,
            unfreeze: unfreeze,
            delete: deleteUser,
            transfer: transfer,
            contactRequest: contactRequest
        };

        return service;


        function getUser(user) {
            return ItemsApi.get({endpoint: 'stations', key: user.station.id, resource: 'users', resource_key: user.id}).$promise;
        }

        function getUserRemoteStatus(user) {
            return ItemsApi.get({endpoint: 'stations', key: user.station.id, resource: 'users', resource_key: user.id, action: 'remote_status'}).$promise;
        }

        function getUsers(filter, config) {
            if (!!filter) {
                config.params = $.extend(config.params, {query: filter});
            }
            var deferred = $q.defer();
            var station_id = config ? (parseInt(config.station_id) || parseInt(config.stationId) || 0) : 0;

            if (myservice.isAdmin() && !station_id) {
                $http.get(MainService.stationsEndpoint()+'/users', config)
                    .then(success)
                    .catch(fail);
            } else {
                station_id = station_id + '/';

                $http.get(MainService.stationsEndpoint()+'/' + station_id + 'users', config)
                    .then(success)
                    .catch(fail);
            }

            return deferred.promise.catch(angular.noop);

            function success(response) {
                deferred.resolve(response.data);
                return response.data;
            }

            function fail(e) {
                deferred.reject();
                return exception.catcher('XHR Failed for getUsers')(e);
            }
        }


        function getUserCount(station_id) {
          var params = {params: {count: '✓'}};
          if (station_id) {
            params.station_id = station_id;
          }
          return getUsers(null, params).then(function (data) {
            return data.meta.total;
          });
        }



        function update(params, args) {
            var station_id = params.station_id
            var user_id = params.id
            delete params.id;
            delete params.station_id;

            return ItemsApi.update({endpoint: 'stations', key: station_id, resource: 'users', resource_key: user_id}, params).$promise;
        }
        function renew(params, args) {
            var station_id = params.station_id
            var user_id = params.id
            delete params.id;
            delete params.station_id;

            return ItemsApi.update({endpoint: 'stations', key: station_id, resource: 'users', resource_key: user_id, action: 'renew'}, params).$promise;
        }

        function apps(params, args) {
            var id = params.id
            delete params.id;
            return ItemsApi.update($.extend({endpoint: 'users', id: id, action: 'apps'}, params)).$promise;
        }

        function offers(params) {
            var station_id = params.station_id
            var id = params.id
            delete params.id;
            delete params.station_id;
            return ItemsApi.query($.extend({endpoint: 'stations', key: station_id,resource: 'users', resource_key: id, action: 'offers'}, params)).$promise;
        }

        function inviteOne(user) {
            var endpoint = 'stations/' + user.station_id + '/invitation';
            return ItemsApi.save($.extend({endpoint: 'stations', id: user.station_id, action: 'invitation'}, {user: user})).$promise;
        }

        function inviteMultiple(params) {
            var deferred = $q.defer();

            params.endpoint = 'stations/' + params.station_id + '/invitations';
            uploadApi.upload(params)
            .then(function(response) {
                console.log('upload', response);
                deferred.resolve(response);
            });
            return deferred.promise.catch(angular.noop);
        }

        function reinvite(user, service) {
            var deferred = $q.defer();

            var params = !!service ? {params: {service: service}} : {};
            var endpoint = 'users/' + user.id + '/reinvite';
            $http.get(MainService.commonApiEndpoint() + endpoint, params)
            .then(function(data) {
                deferred.resolve(data);
            }, function(data) {
                deferred.reject();
            });

            return deferred.promise.catch(angular.noop);
        }

        function reinviteAll(params) {
            var deferred = $q.defer();

            var stationId = params.station_id ? params.station_id+'/' : '';
            var endpoint = 'stations/' + stationId + 'reinvite';
            $http.get(MainService.commonApiEndpoint() + endpoint)
            .then(function(data) {
                deferred.resolve(data);
            }, function(data) {
                deferred.reject();
            });

            return deferred.promise.catch(angular.noop);
        }

        function freezeAll(params) {
            var deferred = $q.defer();

            var stationId = params.station_id ? params.station_id+'/' : '';
            var endpoint = 'stations/' + stationId + 'freeze_inactive_users';
            $http.get(MainService.commonApiEndpoint() + endpoint, {params: {months_ago: params.months_ago}})
            .then(function(data) {
                deferred.resolve(data);
            }, function(data) {
                deferred.reject();
            });

            return deferred.promise.catch(angular.noop);
        }

        function freeze(user, service) {
            var params = !!service ? {params: {service: service}} : {};
            return ItemsApi.get({endpoint: 'stations', key: user.station.id, resource: 'users', resource_key: user.id, action: 'freeze'}, params).$promise;
        }

        function unfreeze(user, service) {
            var params = !!service ? {params: {service: service}} : {};
            return ItemsApi.delete({endpoint: 'stations', key: user.station.id, resource: 'users', resource_key: user.id, action: 'freeze'}, params).$promise;
        }

        function deleteUser(user) {
            return ItemsApi.delete({endpoint: 'stations', key: user.station.id, resource: 'users', resource_key: user.id}).$promise;
        }

        function transfer(user, station) {
            var deferred = $q.defer();
            var endpoint = 'stations/' + station.id + '/users/' + user.id;
            var params = { station_id: station.id };
            $http.put(MainService.commonApiEndpoint() + endpoint, params)
            .then(function(data) {
                deferred.resolve(data);
            }, function(data) {
                deferred.reject();
            });
            return deferred.promise.catch(angular.noop);
        }

        function contactRequest(args) {
            return ItemsApi.save({endpoint: 'users', action: 'contact_request'}, args).$promise;
        }
    }
})();

(function () {
    'use strict';

    angular
        .module('app.widgets')
        .directive('htImgPerson', htImgPerson);

    htImgPerson.$inject = ['config'];
    /*  */
    function htImgPerson (config) {
        //Usage:
        //<img ht-img-person="{{person.imageSource}}"/>
        var basePath = config.imageBasePath;
        var unknownImage = config.unknownPersonImageSource;
        var directive = {
            link: link,
            restrict: 'A'
        };
        return directive;

        function link(scope, element, attrs) {
            attrs.$observe('htImgPerson', function (value) {
                value = basePath + (value || unknownImage);
                attrs.$set('src', value);
            });
        }
    }
})();

(function() {
    'use strict';

    angular
        .module('app.widgets')
        .directive('htWidgetHeaderWithFilter', htWidgetHeader);

    /*  */
    function htWidgetHeader() {
        //Usage:
        //<div ht-widget-header title="vm.map.title"></div>
        // Creates:
        // <div ht-widget-header=""
        //      title="Movie"
        //      allow-collapse="true" </div>
        var directive = {
            scope: {
                'title': '@',
                'subtitle': '@',
                'filter': '=',
                'allowCollapse': '@',
                'count': '=',
                'actions': '='
            },
            templateUrl: 'app/widgets/widget-header-with-filter.html',
            restrict: 'EA',
            controller: ['$scope', '$element', '$attrs', 'myservice', function($scope, $element, $attrs, myservice) {
                var vm = this;
                $scope.$watch('filter', function(v,o) {
                    // console.log('filter', v,o);
                    if (v) {
                        $element.find('.search-input').addClass('simple-open');
                    } else {
                        $element.find('.search-input').removeClass('simple-open');
                    }
                });

                vm.me = myservice.me();

                return vm;
            }],
            controllerAs: 'vm'
        };
        return directive;
    }
})();

(function() {
    'use strict';

    angular
        .module('app.widgets')
        .directive('htWidgetHeader', htWidgetHeader);

    /*  */
    function htWidgetHeader() {
        //Usage:
        //<div ht-widget-header title="vm.map.title"></div>
        // Creates:
        // <div ht-widget-header=""
        //      title="Movie"
        //      allow-collapse="true" </div>
        var directive = {
            scope: {
                'title': '@',
                'subtitle': '@',
                'rightText': '@',
                'allowCollapse': '@'
            },
            templateUrl: 'app/widgets/widget-header.html',
            restrict: 'EA'
        };
        return directive;
    }
})();
