odoo.define('web_editor.summernote', function (require) {
'use strict';

var core = require('web.core');
require('summernote/summernote'); // wait that summernote is loaded

var _t = core._t;

// Summernote Lib (neek hack to make accessible: method and object)
// var agent = $.summernote.core.agent;
var dom = $.summernote.core.dom;
var range = $.summernote.core.range;
var list = $.summernote.core.list;
var key = $.summernote.core.key;
var eventHandler = $.summernote.eventHandler;
var editor = eventHandler.modules.editor;
var renderer = $.summernote.renderer;
var options = $.summernote.options;

// Browser-unify execCommand
var oldJustify = {};
_.each(['Left', 'Right', 'Full', 'Center'], function (align) {
    oldJustify[align] = editor['justify' + align];
    editor['justify' + align] = function ($editable, value) {
        // Before calling the standard function, check all elements which have
        // an 'align' attribute and mark them with their value
        var $align = $editable.find('[align]');
        _.each($align, function (el) {
            var $el = $(el);
            $el.data('__align', $el.attr('align'));
        });

        // Call the standard function
        oldJustify[align].apply(this, arguments);

        // Then:

        // Remove the text-align of elements which lost the 'align' attribute
        var $newAlign = $editable.find('[align]');
        $align.not($newAlign).css('text-align', '');

        // Transform the 'align' attribute into the 'text-align' css
        // property for elements which received the 'align' attribute or whose
        // 'align' attribute changed
        _.each($newAlign, function (el) {
            var $el = $(el);

            var oldAlignValue = $align.data('__align');
            var alignValue = $el.attr('align');
            if (oldAlignValue === alignValue) {
                // If the element already had an 'align' attribute and that it
                // did not changed, do nothing (compatibility)
                return;
            }

            $el.removeAttr('align');
            $el.css('text-align', alignValue);

            // Note the first step (removing the text-align of elemnts which
            // lost the 'align' attribute) is kinda the same as this one, but
            // this one handles the elements which have been edited with chrome
            // or with this new system
            $el.find('*').css('text-align', '');
        });

        // Unmark the elements
        $align.removeData('__align');
    };
});


// Add methods to summernote

dom.hasContentAfter = function (node) {
    var next;
    if (dom.isEditable(node)) return;
    while (node.nextSibling) {
        next = node.nextSibling;
        if (next.tagName || dom.isVisibleText(next) || dom.isBR(next)) return next;
        node = next;
    }
};
dom.hasContentBefore = function (node) {
    var prev;
    if (dom.isEditable(node)) return;
    while (node.previousSibling) {
        prev = node.previousSibling;
        if (prev.tagName || dom.isVisibleText(prev) || dom.isBR(prev)) return prev;
        node = prev;
    }
};
dom.ancestorHaveNextSibling = function (node, pred) {
    pred = pred || dom.hasContentAfter;
    while (!dom.isEditable(node) && (!node.nextSibling || !pred(node))) { node = node.parentNode; }
    return node;
};
dom.ancestorHavePreviousSibling = function (node, pred) {
    pred = pred || dom.hasContentBefore;
    while (!dom.isEditable(node) && (!node.previousSibling || !pred(node))) { node = node.parentNode; }
    return node;
};
dom.nextElementSibling = function (node) {
    while (node) {
        node = node.nextSibling;
        if (node && node.tagName) {
            break;
        }
    }
    return node;
};
dom.previousElementSibling = function (node) {
    while (node) {
        node = node.previousSibling;
        if (node && node.tagName) {
            break;
        }
    }
    return node;
};
dom.lastChild = function (node) {
    while (node.lastChild) { node = node.lastChild; }
    return node;
};
dom.firstChild = function (node) {
    while (node.firstChild) { node = node.firstChild; }
    return node;
};
dom.lastElementChild = function (node, deep) {
    node = deep ? dom.lastChild(node) : node.lastChild;
    return !node || node.tagName ? node : dom.previousElementSibling(node);
};
dom.firstElementChild = function (node, deep) {
    node = deep ? dom.firstChild(node) : node.firstChild;
    return !node || node.tagName ? node : dom.nextElementSibling(node);
};
dom.isEqual = function (prev, cur) {
    if (prev.tagName !== cur.tagName) {
        return false;
    }
    if ((prev.attributes ? prev.attributes.length : 0) !== (cur.attributes ? cur.attributes.length : 0)) {
        return false;
    }

    function strip(text) {
        return text && text.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
    }
    var att, att2;
    loop_prev:
    for (var a in prev.attributes) {
        att = prev.attributes[a];
        for (var b in cur.attributes) {
            att2 = cur.attributes[b];
            if (att.name === att2.name) {
                if (strip(att.value) !== strip(att2.value)) return false;
                continue loop_prev;
            }
        }
        return false;
    }
    return true;
};
dom.hasOnlyStyle = function (node) {
    for (var i = 0; i < node.attributes.length; i++) {
        var attr = node.attributes[i];
        if (attr.attributeName !== 'style') {
            return false;
        }
    }
    return true;
};
dom.hasProgrammaticStyle = function (node) {
    var styles = ["float", "display", "position", "top", "left", "right", "bottom"];
    for (var i = 0; i < node.style.length; i++) {
      var style = node.style[i];
      if (styles.indexOf(style) !== -1) {
          return true;
      }
    }
    return false;
};
dom.mergeFilter = function (prev, cur, parent) {
    // merge text nodes
    if (prev && (dom.isText(prev) || (['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'P'].indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) {
        return true;
    }
    if (prev && prev.tagName === "P" && dom.isText(cur)) {
        return true;
    }
    if (prev && dom.isText(cur) && !dom.isVisibleText(cur) && (dom.isText(prev) || dom.isVisibleText(prev))) {
        return true;
    }
    if (prev && !dom.isBR(prev) && dom.isEqual(prev, cur) &&
        ((prev.tagName && dom.getComputedStyle(prev).display === "inline" &&
          cur.tagName && dom.getComputedStyle(cur).display === "inline"))) {
        return true;
    }
    if (dom.isEqual(parent, cur) &&
        ((parent.tagName && dom.getComputedStyle(parent).display === "inline" &&
          cur.tagName && dom.getComputedStyle(cur).display === "inline"))) {
        return true;
    }
    if (parent && cur.tagName === "FONT" && (!cur.firstChild || (!cur.attributes.getNamedItem('style') && !cur.className.length))) {
        return true;
    }
    // On backspace, webkit browsers create a <span> with a bunch of
    // inline styles "remembering" where they come from.
    // chances are we had e.g.
    //  <p>foo</p>
    //  <p>bar</p>
    // merged the lines getting this in webkit
    //  <p>foo<span>bar</span></p>
    if (parent && cur.tagName === "SPAN" && dom.hasOnlyStyle(cur) && !dom.hasProgrammaticStyle(cur)) {
        return true;
    }
};
dom.doMerge = function (prev, cur) {
    if (prev.tagName) {
        if (prev.childNodes.length && !prev.textContent.match(/\S/) && dom.firstElementChild(prev) && dom.isBR(dom.firstElementChild(prev))) {
            prev.removeChild(dom.firstElementChild(prev));
        }
        if (cur.tagName) {
            while (cur.firstChild) {
                prev.appendChild(cur.firstChild);
            }
            cur.parentNode.removeChild(cur);
        } else {
            prev.appendChild(cur);
        }
    } else {
        if (cur.tagName) {
            var deep = cur;
            while (deep.tagName && deep.firstChild) {deep = deep.firstChild;}
            prev.appendData(deep.textContent);
            cur.parentNode.removeChild(cur);
        } else {
            prev.appendData(cur.textContent);
            cur.parentNode.removeChild(cur);
        }
    }
};
dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
    mergeFilter = mergeFilter || dom.mergeFilter;
    var _merged = false;
    var add = all || false;

    if (!begin) {
        begin = node;
        while (begin.firstChild) {begin = begin.firstChild;}
        so = 0;
    } else if (begin.tagName && begin.childNodes[so]) {
        begin = begin.childNodes[so];
        so = 0;
    }
    if (!end) {
        end = node;
        while (end.lastChild) {end = end.lastChild;}
        eo = end.textContent.length-1;
    } else if (end.tagName && end.childNodes[so]) {
        end = end.childNodes[so];
        so = 0;
    }

    begin = dom.firstChild(begin);
    if (dom.isText(begin) && so > begin.textContent.length) {
        so = 0;
    }
    end = dom.firstChild(end);
    if (dom.isText(end) && eo > end.textContent.length) {
        eo = 0;
    }

    function __merge(node) {
        var merged = false;
        var prev;
        for (var k=0; k<node.childNodes.length; k++) {
            var cur = node.childNodes[k];

            if (cur === begin) {
                if (!all) add = true;
            }

            __merge(cur);
            dom.orderClass(dom.node(cur));

            if (!add || !cur) continue;
            if (cur === end) {
                if (!all) add = false;
            }

            // create the first prev value
            if (!prev) {
                if (mergeFilter.call(dom, prev, cur, node)) {
                    prev = prev || cur.previousSibling;
                    dom.moveTo(cur, cur.parentNode, cur);
                    k--;
                } else {
                    prev = cur;
                }
                continue;
            } else if (mergeFilter.call(dom, null, cur, node)) { // merge with parent
                prev = prev || cur.previousSibling;
                dom.moveTo(cur, cur.parentNode, cur);
                k--;
                continue;
            }

            // merge nodes
            if (mergeFilter.call(dom, prev, cur, node)) {
                var p = prev;
                var c = cur;
                // compute prev/end and offset
                if (prev.tagName) {
                    if (cur.tagName) {
                        if (cur === begin) begin = prev;
                        if (cur === end) end = prev;
                    }
                } else {
                    if (cur.tagName) {
                        var deep = cur;
                        while (deep.tagName && deep.lastChild) {deep = deep.lastChild;}
                        if (deep === begin) {
                            so += prev.textContent.length;
                            begin = prev;
                        }
                        if (deep === end) {
                            eo += prev.textContent.length;
                            end = prev;
                        }
                    } else {
                        // merge text nodes
                        if (cur === begin) {
                            so += prev.textContent.length;
                            begin = prev;
                        }
                        if (cur === end) {
                            eo += prev.textContent.length;
                            end = prev;
                        }
                    }
                }

                dom.doMerge(p, c);

                merged = true;
                k--;
                continue;
            }

            prev = cur;
        }

        // an other loop to merge the new shibbing nodes
        if (merged) {
            _merged = true;
            __merge(node);
        }
    }
    if (node) {
        __merge(node);
    }

    return {
        merged: _merged,
        sc: begin,
        ec: end,
        so: so,
        eo: eo
    };
};
dom.autoMerge = function (target, previous) {
    var node = dom.lastChild(target);
    var nodes = [];
    var temp;

    while (node) {
        nodes.push(node);
        temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node));
        if (temp) {
            if (!dom.isText(node) && !dom.isMergable(node) && temp.tagName !== node.tagName) {
                nodes = [];
            }
            break;
        }
        node = node.parentNode;
    }

    while (nodes.length) {
        node = nodes.pop();
        if (node && (temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node))) &&
            temp.tagName === node.tagName &&
            !dom.isText(node) &&
            dom.isMergable(node) &&
            !dom.isNotBreakable(node) && !dom.isNotBreakable(previous ? dom.previousElementSibling(node) : dom.nextElementSibling(node))) {

            if (previous) {
                dom.doMerge(temp, node);
            } else {
                dom.doMerge(node, temp);
            }
        }
    }
};
dom.removeSpace = function (node, begin, so, end, eo) {
    var removed = false;
    var add = node === begin;

    if (node === begin && begin === end && dom.isBR(node)) {
        return {
            removed: removed,
            sc: begin,
            ec: end,
            so: so,
            eo: eo
        };
    }

    (function __remove_space(node) {
        if (!node) return;
        var t_begin, t_end;
        for (var k=0; k<node.childNodes.length; k++) {
            var cur = node.childNodes[k];

            if (cur === begin) add = true;

            if (cur.tagName && cur.tagName !== "SCRIPT" && cur.tagName !== "STYLE" && dom.getComputedStyle(cur).whiteSpace !== "pre") {
                __remove_space(cur);
            }

            if (!add) continue;
            if (cur === end) add = false;

            // remove begin empty text node
            if (node.childNodes.length > 1 && dom.isText(cur) && !dom.isVisibleText(cur)) {
                removed = true;
                if (cur === begin) {
                        t_begin = dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur));
                        if (t_begin) {
                            so = 0;
                            begin = dom.lastChild(t_begin);
                        }
                }
                if (cur === end) {
                        t_end = dom.hasContentAfter(dom.ancestorHaveNextSibling(cur));
                        if (t_end) {
                            eo = 1;
                            end = dom.firstChild(t_end);
                            if (dom.isText(end)) {
                                eo = end.textContent.length;
                            }
                    }
                }
                cur.parentNode.removeChild(cur);
                begin = dom.lastChild(begin);
                end = dom.lastChild(end);
                k--;
                continue;
            }

            // convert HTML space
            if (dom.isText(cur)) {
                var text;
                var temp;
                var _temp;
                var exp1 = /[\t\n\r ]+/g;
                var exp2 = /(?!([ ]|\u00A0)|^)\u00A0(?!([ ]|\u00A0)|$)/g;
                if (cur === begin) {
                    temp = cur.textContent.substr(0, so);
                    _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
                    so -= temp.length - _temp.length;
                }
                if (cur === end) {
                    temp = cur.textContent.substr(0, eo);
                    _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
                    eo -= temp.length - _temp.length;
                }
                text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
                removed = removed || cur.textContent.length !== text.length;
                cur.textContent = text;
            }
        }
    })(node);

    return {
        removed: removed,
        sc: begin,
        ec: end,
        so: !dom.isBR(begin) && so > 0 ? so : 0,
        eo: dom.isBR(end) ? 0 : eo
    };
};
dom.removeBetween = function (sc, so, ec, eo, towrite) {
    var text;
    if (ec.tagName) {
        if (ec.childNodes[eo]) {
            ec = ec.childNodes[eo];
            eo = 0;
        } else {
            ec = dom.lastChild(ec);
            eo = dom.nodeLength(ec);
        }
    }
    if (sc.tagName) {
        sc = sc.childNodes[so] || dom.firstChild(ec);
        so = 0;
        if (!dom.hasContentBefore(sc) && towrite) {
            sc.parentNode.insertBefore(document.createTextNode('\u00A0'), sc);
        }
    }
    if (!eo && sc !== ec) {
        ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(ec)) || ec);
        eo = ec.textContent.length;
    }

    var ancestor = dom.commonAncestor(sc.tagName ? sc.parentNode : sc, ec.tagName ? ec.parentNode : ec) || dom.ancestor(sc, dom.isEditable);

    if (!dom.isContentEditable(ancestor)) {
        return {
            sc: sc,
            so: so,
            ec: sc,
            eo: eo
        };
    }

    if (ancestor.tagName) {
        var ancestor_sc = sc;
        var ancestor_ec = ec;
        while (ancestor !== ancestor_sc && ancestor !== ancestor_sc.parentNode) { ancestor_sc = ancestor_sc.parentNode; }
        while (ancestor !== ancestor_ec && ancestor !== ancestor_ec.parentNode) { ancestor_ec = ancestor_ec.parentNode; }


        var node = dom.node(sc);
        if (!dom.isNotBreakable(node) && !dom.isVoid(sc)) {
            sc = dom.splitTree(ancestor_sc, {'node': sc, 'offset': so});
        }
        var before = dom.hasContentBefore(dom.ancestorHavePreviousSibling(sc));

        var after;
        if (ec.textContent.slice(eo, Infinity).match(/\S|\u00A0/)) {
            after = dom.splitTree(ancestor_ec, {'node': ec, 'offset': eo});
        } else {
            after = dom.hasContentAfter(dom.ancestorHaveNextSibling(ec));
        }

        var nodes = dom.listBetween(sc, ec);

        var ancestor_first_last = function (node) {
            return node === before || node === after;
        };

        for (var i=0; i<nodes.length; i++) {
            if (!dom.ancestor(nodes[i], ancestor_first_last) && !$.contains(nodes[i], before) && !$.contains(nodes[i], after) && !dom.isEditable(nodes[i])) {
                nodes[i].parentNode.removeChild(nodes[i]);
            }
        }

        if (dom.listAncestor(after).length  <= dom.listAncestor(before).length) {
            sc = dom.lastChild(before || ancestor);
            so = dom.nodeLength(sc);
        } else {
            sc = dom.firstChild(after);
            so = 0;
        }

        if (dom.isVoid(node)) {
            // we don't need to append a br
        } else if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) {
            var br = $("<br/>")[0];
            node.appendChild(sc);
            sc = br;
            so = 0;
        } else if (!ancestor.children.length && !ancestor.textContent.match(/\S|\u00A0/)) {
            sc = $("<br/>")[0];
            so = 0;
            $(ancestor).prepend(sc);
        } else if (dom.isText(sc)) {
            text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
            so = Math.min(so, text.length);
            sc.textContent = text;
        }
    } else {
        text = ancestor.textContent;
        ancestor.textContent = text.slice(0, so) + text.slice(eo, Infinity).replace(/^[ \t\n\r]+/, '\u00A0');
    }

    eo = so;
    if (!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
        ancestor = dom.node(sc);
        text = document.createTextNode('\u00A0');
        $(sc).before(text);
        sc = text;
        so = 0;
        eo = 1;
    }

    var parentNode = sc && sc.parentNode;
    if (parentNode && sc.tagName === 'BR') {
        sc = parentNode;
        ec = parentNode;
    }

    return {
        sc: sc,
        so: so,
        ec: sc,
        eo: eo
    };
};
dom.indent = function (node) {
    var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
    var margin = parseFloat(node.style[style] || 0)+1.5;
    node.style[style] = margin + "em";
    return margin;
};
dom.outdent = function (node) {
    var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
    var margin = parseFloat(node.style[style] || 0)-1.5;
    node.style[style] = margin > 0 ? margin + "em" : "";
    return margin;
};
dom.scrollIntoViewIfNeeded = function (node) {
    node = dom.node(node);

    var $span;
    if (dom.isBR(node)) {
        $span = $('<span/>').text('\u00A0');
        $(node).after($span);
        node = $span[0];
    }

    if (node.scrollIntoViewIfNeeded) {
        node.scrollIntoViewIfNeeded(false);
    } else {
        var offsetParent = node.offsetParent;
        while (offsetParent) {
            var elY = 0;
            var elH = node.offsetHeight;
            var parent = node;

            while (offsetParent && parent) {
                elY += node.offsetTop;

                // get if a parent have a scrollbar
                parent = node.parentNode;
                while (parent !== offsetParent &&
                    (parent.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(parent).overflowY) === -1)) {
                    parent = parent.parentNode;
                }
                node = parent;

                if (parent !== offsetParent) {
                    elY -= parent.offsetTop;
                    parent = null;
                }

                offsetParent = node.offsetParent;
            }

            if ((node.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(node).overflowY) !== -1) &&
                (node.scrollTop + node.clientHeight) < (elY + elH)) {
                node.scrollTop = (elY + elH) - node.clientHeight;
            }
        }
    }

    if ($span) {
        $span.remove();
    }

    return;
};
dom.moveTo = function (node, target, before) {
    var nodes = [];
    while (node.firstChild) {
        nodes.push(node.firstChild);
        if (before) {
            target.insertBefore(node.firstChild, before);
        } else {
            target.appendChild(node.firstChild);
        }
    }
    node.parentNode.removeChild(node);
    return nodes;
};
dom.isMergable = function (node) {
    return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font".indexOf(node.tagName.toLowerCase()) !== -1;
};
dom.isSplitable = function (node) {
    return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a font".indexOf(node.tagName.toLowerCase()) !== -1;
};
dom.isRemovableEmptyNode = function (node) {
    return "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font span br".indexOf(node.tagName.toLowerCase()) !== -1;
};
dom.isForbiddenNode = function (node) {
    return node.tagName === "BR" || $(node).is(".fa, img");
};
/**
 * @todo 'so' and 'eo' were added as a bugfix and are not given everytime. They
 * however should be as the function may be wrong without them (for example,
 * when asking the list between an element and its parent, as there is no path
 * from the beginning of the former to the beginning of the later).
 */
dom.listBetween = function (sc, ec, so, eo) {
    var nodes = [];
    var ancestor = dom.commonAncestor(sc, ec);
    dom.walkPoint({'node': sc, 'offset': so || 0}, {'node': ec, 'offset': eo || 0}, function (point) {
        if (ancestor !== point.node || ancestor === sc || ancestor === ec) {
            nodes.push(point.node);
        }
    });
    return list.unique(nodes);
};
dom.isNotBreakable = function (node) {
    // avoid triple click => crappy dom
    return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node));
};
dom.isContentEditable = function (node) {
    return $(node).closest('[contenteditable]').prop('contenteditable') === 'true';
};
dom.isContentEditableFalse = function (node) {
    return $(node).closest('[contenteditable]').prop('contenteditable') === 'false';
};
dom.isFont = function (node) {
    var nodeName = node && node.nodeName.toUpperCase();
    return node && (nodeName === "FONT" ||
        (nodeName === "SPAN" && (
            node.className.match(/(^|\s)fa(\s|$)/i) ||
            node.className.match(/(^|\s)(text|bg)-/i) ||
            (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color|font-size):/i)))) );
};
dom.isVisibleText = function (textNode) {
  return !!textNode.textContent.match(/\S|\u00A0/);
};
var old_isVisiblePoint = dom.isVisiblePoint;
dom.isVisiblePoint = function (point) {
  return point.node.nodeType !== 8 && old_isVisiblePoint.apply(this, arguments);
};
dom.orderStyle = function (node) {
  var style = node.getAttribute('style');
  if (!style) return null;
  style = style.replace(/[\s\n\r]+/, ' ').replace(/^ ?;? ?| ?;? ?$/g, '').replace(/ ?; ?/g, ';');
  if (!style.length) {
      node.removeAttribute("style");
      return null;
  }
  style = style.split(";");
  style.sort();
  style = style.join("; ")+";";
  node.setAttribute('style', style);
  return style;
};
dom.orderClass = function (node) {
    var className = node.getAttribute && node.getAttribute('class');
    if (!className) return null;
    className = className.replace(/[\s\n\r]+/, ' ').replace(/^ | $/g, '').replace(/ +/g, ' ');
    if (!className.length) {
        node.removeAttribute("class");
        return null;
    }
    className = className.split(" ");
    className.sort();
    className = className.join(" ");
    node.setAttribute('class', className);
    return className;
};
dom.node = function (node) {
    return dom.isText(node) ? node.parentNode : node;
};
dom.moveContent = function (from, to) {
  if (from === to) {
    return;
  }
  if (from.parentNode === to) {
    while (from.lastChild) {
      dom.insertAfter(from.lastChild, from);
    }
  } else {
    while (from.firstChild && from.firstChild !== to) {
      to.appendChild(from.firstChild);
    }
  }
};
dom.getComputedStyle = function (node) {
    return node.nodeType === Node.COMMENT_NODE ? {} : window.getComputedStyle(node);
};

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
    var sc = this.sc;
    var so = this.so;
    var ec = this.ec;
    var eo = this.eo;
    isNotBreakable = isNotBreakable || dom.isNotBreakable;

    // search the first snippet editable node
    var start = keep_end ? ec : sc;
    while (start) {
        if (isNotBreakable(start, sc, so, ec, eo)) {
            break;
        }
        start = start.parentNode;
    }

    // check if the end caret have the same node
    var lastFilterEnd;
    var end = keep_end ? sc : ec;
    while (end) {
        if (start === end) {
            break;
        }
        if (isNotBreakable(end, sc, so, ec, eo)) {
            lastFilterEnd = end;
        }
        end = end.parentNode;
    }
    if (lastFilterEnd) {
        end = lastFilterEnd;
    }
    if (!end) {
        end = document.getElementsByTagName('body')[0];
    }

    // if same node, keep range
    if (start === end || !start) {
        return this;
    }

    // reduce or extend the range to don't break a isNotBreakable area
    if ($.contains(start, end)) {

        if (keep_end) {
                sc = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || sc);
            so = sc.textContent.length;
        } else if (!eo) {
                ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || ec);
            eo = ec.textContent.length;
        } else {
                ec = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(end)) || ec);
            eo = 0;
        }
    } else {

        if (keep_end) {
            sc = dom.firstChild(start);
            so = 0;
        } else {
            ec = dom.lastChild(start);
            eo = ec.textContent.length;
        }
    }

    return new range.WrappedRange(sc, so, ec, eo);
};
/**
 * Returns the image the range is in or matches (if any, false otherwise).
 *
 * @todo this implementation may not cover all corner cases but should do the
 * trick for all reproductible ones
 * @returns {DOMElement|boolean}
 */
range.WrappedRange.prototype.isOnImg = function () {
    // If not a selection but a cursor position, just check if a point's
    // ancestor is an image or not
    if (this.sc === this.ec && this.so === this.eo) {
        return dom.ancestor(this.sc, dom.isImg);
    }

    var startPoint = {node: this.sc, offset: this.so};
    var endPoint = {node: this.ec, offset: this.eo};

    var nb = 0;
    var image;
    var textNode;
    dom.walkPoint(startPoint, endPoint, function (point) {
        // If the element has children (not a text node and not empty node),
        // the element cannot be considered as selected (these children will
        // be processed to determine that)
        if (dom.hasChildren(point.node)) {
            return;
        }

        // Check if an ancestor of the current point is an image
        var pointImg = dom.ancestor(point.node, dom.isImg);
        var isText = dom.isText(point.node);

        // Check if a visible element is selected, i.e.
        // - If an ancestor of the current is an image we did not see yet
        // - If the point is not in a br or a text (so a node with no children)
        // - If the point is in a non empty text node we already saw
        if (pointImg ?
            (image !== pointImg) :
            ((!dom.isBR(point.node) && !isText) || (textNode === point.node && point.node.textContent.match(/\S|\u00A0/)))) {
            nb++;
        }

        // If an ancestor of the current point is an image, then save it as the
        // image we are looking for
        if (pointImg) {
            image = pointImg;
        }
        // If the current point is a text node save it as the last text node
        // seen (if we see it again, this might mean it is selected)
        if (isText) {
            textNode = point.node;
        }
    });

    return nb === 1 && image;
};
range.WrappedRange.prototype.deleteContents = function (towrite) {
    if (this.sc === this.ec && this.so === this.eo) {
        return this;
    }

    var r;
    var image = this.isOnImg();
    if (image) {
        // If the range matches/is in an image, then the image is to be removed
        // and the cursor moved to its previous position
        var parentNode = image.parentNode;
        var index = _.indexOf(parentNode.childNodes, image);
        parentNode.removeChild(image);
        r = new range.WrappedRange(parentNode, index, parentNode, index);
    } else {
        r = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
    }

    $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
    return new range.WrappedRange(r.sc, r.so, r.ec, r.eo);
};
range.WrappedRange.prototype.clean = function (mergeFilter, all) {
    var node = dom.node(this.sc === this.ec ? this.sc : this.commonAncestor());
        node = node || $(this.sc).closest('[contenteditable]')[0];
    if (node.childNodes.length <=1) {
        return this;
    }

    var merge = dom.merge(node, this.sc, this.so, this.ec, this.eo, mergeFilter, all);
    var rem = dom.removeSpace(node.parentNode, merge.sc, merge.so, merge.ec, merge.eo);

    if (merge.merged || rem.removed) {
        return range.create(rem.sc, rem.so, rem.ec, rem.eo);
    }
    return this;
};
range.WrappedRange.prototype.remove = function (mergeFilter) {
};
range.WrappedRange.prototype.isOnCellFirst = function () {
    var node = dom.ancestor(this.sc, function (node) {return ["LI", "DIV", "TD","TH"].indexOf(node.tagName) !== -1;});
    return node && ["TD","TH"].indexOf(node.tagName) !== -1;
};
range.WrappedRange.prototype.isContentEditable = function () {
    return dom.isContentEditable(this.sc) && (this.sc === this.ec || dom.isContentEditable(this.ec));
};

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

renderer.tplButtonInfo.fontsize = function (lang, options) {
    var items = options.fontSizes.reduce(function (memo, v) {
        return memo + '<a data-event="fontSize" href="#" class="dropdown-item" data-value="' + v + '">' +
                  '<i class="fa fa-check"></i> ' + v +
                '</a>';
    }, '');

    var sLabel = '<span class="note-current-fontsize">11</span>';
    return renderer.getTemplate().button(sLabel, {
        title: lang.font.size,
        dropdown: '<div class="dropdown-menu">' + items + '</div>'
    });
};

renderer.tplButtonInfo.color = function (lang, options) {
    var foreColorButtonLabel = '<i class="' + options.iconPrefix + options.icons.color.recent + '"></i>';
    var backColorButtonLabel = '<i class="' + options.iconPrefix + 'paint-brush"></i>';
    // TODO Remove recent color button if possible.
    // It is still put to avoid JS errors when clicking other buttons as the
    // editor still expects it to exist.
    var recentColorButton = renderer.getTemplate().button(foreColorButtonLabel, {
        className: 'note-recent-color d-none',
        title: lang.color.foreground,
        event: 'color',
        value: '{"backColor":"#B35E9B"}'
    });
    var foreColorItems = [
        '<li><div class="btn-group flex-column">',
        '<div class="note-color-palette" data-target-event="foreColor"></div>',
        '<h6 class="note-custom-color mt8" data-event="customColor" data-value="foreColor" title="' + lang.color.custom + '">',
        lang.color.custom + '</h6>',
        '<div class="note-custom-color-palette" data-target-event="foreColor"></div>',
        '</div></li>',
    ];
    var backColorItems = [
        '<li><div class="btn-group flex-column">',
        '<div class="note-color-reset" data-event="backColor" data-value="inherit" title="' + lang.color.transparent + '">',
        lang.color.setTransparent + '</div>',
        '<div class="note-color-palette" data-target-event="backColor"></div>',
        '<h6 class="note-custom-color mt8" data-event="customColor" data-value="backColor" title="' + lang.color.custom + '">',
        lang.color.custom + '</h6>',
        '<div class="note-custom-color-palette" data-target-event="backColor"></div>',
        '</div></li>',
    ];
    var foreColorButton = renderer.getTemplate().button(foreColorButtonLabel, {
        className: 'note-fore-color-preview mx-1',
        title: lang.color.foreground,
        dropdown: renderer.getTemplate().dropdown(foreColorItems)
    });
    var backColorButton = renderer.getTemplate().button(backColorButtonLabel, {
        className: 'note-back-color-preview mx-1',
        title: lang.color.background,
        dropdown: renderer.getTemplate().dropdown(backColorItems)
    });
    return recentColorButton + foreColorButton + backColorButton;
},

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

key.nameFromCode[46] = 'DELETE';
key.nameFromCode[27] = 'ESCAPE';

options.keyMap.pc['BACKSPACE'] = 'backspace';
options.keyMap.pc['DELETE'] = 'delete';
options.keyMap.pc['ENTER'] = 'enter';
options.keyMap.pc['ESCAPE'] = 'cancel';
options.keyMap.mac['SHIFT+TAB'] = 'untab';
options.keyMap.pc['UP'] = 'up';
options.keyMap.pc['DOWN'] = 'down';

options.keyMap.mac['BACKSPACE'] = 'backspace';
options.keyMap.mac['DELETE'] = 'delete';
options.keyMap.mac['ENTER'] = 'enter';
options.keyMap.mac['ESCAPE'] = 'cancel';
options.keyMap.mac['UP'] = 'up';
options.keyMap.mac['DOWN'] = 'down';

options.styleTags = ['p', 'pre', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'];

$.summernote.pluginEvents.insertTable = function (event, editor, layoutInfo, sDim) {
  var $editable = layoutInfo.editable();
  var dimension = sDim.split('x');
  var r = range.create();
  if (!r) return;
  r = r.deleteContents(true);

  var table = editor.table.createTable(dimension[0], dimension[1]);
  var parent = r.sc;
  while (dom.isText(parent.parentNode) || dom.isRemovableEmptyNode(parent.parentNode)) {
    parent = parent.parentNode;
  }
  var node = dom.splitTree(parent, {'node': r.sc, 'offset': r.so}) || r.sc;
  node.parentNode.insertBefore(table, node);

  if ($(node).text() === '' || node.textContent === '\u00A0') {
    node.parentNode.removeChild(node);
  }

  editor.afterCommand($editable);
  event.preventDefault();
  return false;
};
$.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable, 'tab');
    var r = range.create();
    outdent = outdent || false;
    event.preventDefault();

    if (r && (dom.ancestor(r.sc, dom.isCell) || dom.ancestor(r.ec, dom.isCell))) {
        if (r.isCollapsed() && r.isOnCell() && r.isOnCellFirst()) {
            var td = dom.ancestor(r.sc, dom.isCell);
            if (!outdent && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode)) {
                var last = dom.lastChild(td);
                range.create(last, dom.nodeLength(last), last, dom.nodeLength(last)).select();
                $.summernote.pluginEvents.enter(event, editor, layoutInfo);
            } else if (outdent && !dom.previousElementSibling(td) && !$(td.parentNode).text().match(/\S/)) {
                $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
            } else {
                editor.table.tab(r, outdent);
            }
        } else {
            $.summernote.pluginEvents.indent(event, editor, layoutInfo, outdent);
        }
    } else if (r && r.isCollapsed()) {
        if (!r.sc.textContent.slice(0,r.so).match(/\S/) && r.isOnList()) {
            if (outdent) {
                $.summernote.pluginEvents.outdent(event, editor, layoutInfo);
            } else {
                $.summernote.pluginEvents.indent(event, editor, layoutInfo);
            }
        } else {
            var next;
            if (!outdent) {
                if (dom.isText(r.sc)) {
                    next = r.sc.splitText(r.so);
                } else {
                    next = document.createTextNode('');
                    $(r.sc.childNodes[r.so]).before(next);
                }
                editor.typing.insertTab($editable, r, options.tabsize);
                r = range.create(next, 0, next, 0);
                r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
                range.create(r.sc, r.so, r.ec, r.eo).select();
            } else {
                r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
                r = range.create(r.sc, r.so, r.ec, r.eo);
                if (r.sc.splitText) {
                    next = r.sc.splitText(r.so);
                    r.sc.textContent = r.sc.textContent.replace(/(\u00A0)+$/g, '');
                    next.textContent = next.textContent.replace(/^(\u00A0)+/g, '');
                    range.create(r.sc, r.sc.textContent.length, r.sc, r.sc.textContent.length).select();
                }
            }
        }
    }
    return false;
};
$.summernote.pluginEvents.untab = function (event, editor, layoutInfo) {
    return $.summernote.pluginEvents.tab(event, editor, layoutInfo, true);
};
$.summernote.pluginEvents.up = function (event, editor, layoutInfo) {
    var r = range.create();
    var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
    if (!r.isOnCell()) {
        return;
    }
    // check if an ancestor between node and cell has content before
    var ancestor = dom.ancestor(node, function (ancestorNode) {
        return dom.hasContentBefore(ancestorNode) || dom.isCell(ancestorNode);
    });
    if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentBefore(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentBefore(dom.hasContentBefore(ancestor)))) {
        return;
    }
    event.preventDefault();
    var td = dom.ancestor(r.sc, dom.isCell);
    var tr = td.parentNode;
    var target = tr.previousElementSibling && tr.previousElementSibling.children[_.indexOf(tr.children, td)];
    if (!target) {
        target = (dom.ancestorHavePreviousSibling(tr) || tr).previousSibling;
    }
    if (target) {
        range.create(dom.lastChild(target), dom.lastChild(target).textContent.length).select();
    }
};
$.summernote.pluginEvents.down = function (event, editor, layoutInfo) {
    var r = range.create();
    var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
    if (!r.isOnCell()) {
        return;
    }
    // check if an ancestor between node and cell has content after
    var ancestor = dom.ancestor(node, function (ancestorNode) {
        return dom.hasContentAfter(ancestorNode) || dom.isCell(ancestorNode);
    });
    if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentAfter(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentAfter(dom.hasContentAfter(ancestor)))) {
        return;
    }
    event.preventDefault();
    var td = dom.ancestor(r.sc, dom.isCell);
    var tr = td.parentNode;
    var target = tr.nextElementSibling && tr.nextElementSibling.children[_.indexOf(tr.children, td)];
    if (!target) {
        target = (dom.ancestorHaveNextSibling(tr) || tr).nextSibling;
    }
    if (target) {
        range.create(dom.firstChild(target), 0).select();
    }
};
$.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable, 'enter');

    var r = range.create();
    if (!r.isContentEditable()) {
        event.preventDefault();
        return false;
    }
    if (!r.isCollapsed()) {
        r = r.deleteContents();
        r.select();
    }

    var br = $("<br/>")[0];

    var node;
    var $node;
    var $clone;
    var contentBefore = r.sc.textContent.slice(0,r.so).match(/\S|\u00A0/);
    if (!contentBefore && dom.isText(r.sc)) {
        node = r.sc.previousSibling;
        while (!contentBefore && node && dom.isText(node)) {
            contentBefore = dom.isVisibleText(node);
            node = node.previousSibling;
        }
    }

    node = dom.node(r.sc);
    var exist = r.sc.childNodes[r.so] || r.sc;
    exist = dom.isVisibleText(exist) || dom.isBR(exist) ? exist : dom.hasContentAfter(exist) || (dom.hasContentBefore(exist) || exist);

    // table: add a tr
    var td = dom.ancestor(node, dom.isCell);
    if (td && !dom.nextElementSibling(node) && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode) && (!dom.isText(r.sc) || !r.sc.textContent.slice(r.so).match(/\S|\u00A0/))) {
        $node = $(td.parentNode);
        $clone = $node.clone();
        $clone.children().html(dom.blank);
        $node.after($clone);
        node = dom.firstElementChild($clone[0]) || $clone[0];
        range.create(node, 0, node, 0).select();
        dom.scrollIntoViewIfNeeded(br);
        event.preventDefault();
        return false;
    }

    var last = node;
    while (node && dom.isSplitable(node) && !dom.isList(node)) {
        last = node;
        node = node.parentNode;
    }

    if (last === node && !dom.isBR(node)) {
        node = r.insertNode(br, true);
        if (isFormatNode(last.firstChild) && $(last).closest(options.styleTags.join(',')).length) {
            dom.moveContent(last.firstChild, last);
            last.removeChild(last.firstChild);
        }
        do {
            node = dom.hasContentAfter(node);
        } while (node && dom.isBR(node));

        // create an other br because the user can't see the new line with only br in a block
        if (!node && (!br.nextElementSibling || !dom.isBR(br.nextElementSibling))) {
            $(br).before($("<br/>")[0]);
        }
        node = br.nextSibling || br;
    } else if (last === node && dom.isBR(node)) {
        $(node).after(br);
        node = br;
    } else if (!r.so && r.isOnList() && !r.sc.textContent.length && !dom.ancestor(r.sc, dom.isLi).nextElementSibling) {
        // double enter on the end of a list = new line out of the list
        $('<p></p>').append(br).insertAfter(dom.ancestor(r.sc, dom.isList));
        node = br;
    } else if (dom.isBR(exist) && $(r.sc).closest('blockquote, pre').length && !dom.hasContentAfter($(exist.parentNode).closest('blockquote *, pre *').length ? exist.parentNode : exist)) {
        // double enter on the end of a blockquote & pre = new line out of the list
        $('<p></p>').append(br).insertAfter($(r.sc).closest('blockquote, pre'));
        node = br;
    } else if (dom.isEditable(dom.node(r.sc))) {
        // if we are directly in an editable, only SHIFT + ENTER should add a newline
        node = null;
    } else if (last === r.sc) {
        if (dom.isBR(last)) {
            last = last.parentNode;
        }
        $node = $(last);
        $clone = $node.clone().text("");
        $node.after($clone);
        node = dom.node(dom.firstElementChild($clone[0]) || $clone[0]);
        $(node).html(br);
        node = br;
    } else {
        node = dom.splitTree(last, {'node': r.sc, 'offset': r.so}) || r.sc;
        if (!contentBefore) {
            // dom.node chooses the parent if node is text
            var cur = dom.node(dom.lastChild(node.previousSibling));
            if (!dom.isBR(cur)) {
                // We should concat what was before with a <br>
                $(cur).html(cur.innerHTML + br.outerHTML);
            }
        }
        if (!dom.isVisibleText(node)) {
            node = dom.firstChild(node);
            $(dom.node( dom.isBR(node) ? node.parentNode : node )).html(br);
            node = br;
        }
    }

    if (node) {
        node = dom.firstChild(node);
        if (dom.isBR(node)) {
            range.createFromNode(node).select();
        } else {
            range.create(node,0).select();
        }
        dom.scrollIntoViewIfNeeded(node);
    }
    event.preventDefault();
    return false;
};
$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable, "visible");

    var r = range.create();
    if (!r) return;

    if (!r.isCollapsed()) {
        if ((dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) && dom.node(r.sc) !== dom.node(r.ec)) {
            remove_table_content(r);
            r = range.create(r.ec, 0);
        }
        r.select();
    }

    // don't write in forbidden tag (like span for font awsome)
    var node = dom.firstChild(r.sc.tagName && r.so ? r.sc.childNodes[r.so] || r.sc : r.sc);
    while (node.parentNode) {
        if (dom.isForbiddenNode(node)) {
            var text = node.previousSibling;
            if (text && dom.isText(text) && dom.isVisibleText(text)) {
                range.create(text, text.textContent.length, text, text.textContent.length).select();
            } else {
                text = node.parentNode.insertBefore(document.createTextNode( "." ), node);
                range.create(text, 1, text, 1).select();
                setTimeout(function () {
                    var text = range.create().sc;
                    text.textContent = text.textContent.replace(/^./, '');
                    range.create(text, text.textContent.length, text, text.textContent.length).select();
                },0);
            }
            break;
        }
        node = node.parentNode;
    }

    return true;
};

function remove_table_content(r) {
    var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
    if (dom.isText(r.sc)) {
        r.sc.textContent = r.sc.textContent.slice(0, r.so);
    }
    if (dom.isText(r.ec)) {
        r.ec.textContent = r.ec.textContent.slice(r.eo);
    }
    for (var i in nodes) {
        var node = nodes[i];
        if (node === r.sc || node === r.ec || $.contains(node, r.sc) || $.contains(node, r.ec)) {
            continue;
        } else if (dom.isCell(node)) {
            $(node).html("<br/>");
        } else if (node.parentNode) {
            do {
                var parent = node.parentNode;
                parent.removeChild(node);
                node = parent;
            } while (!dom.isVisibleText(node) && !dom.firstElementChild(node) &&
                !dom.isCell(node) &&
                node.parentNode && !$(node.parentNode).hasClass('o_editable'));
        }
    }
    return false;
}

$.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable, "delete");

    var r = range.create();
    if (!r) return;
    if (!r.isContentEditable()) {
        event.preventDefault();
        return false;
    }
    if (!r.isCollapsed()) {
        if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
            remove_table_content(r);
            range.create(r.ec, 0).select();
        } else {
            r = r.deleteContents();
            r.select();
        }
        event.preventDefault();
        return false;
    }

    var target = r.ec;
    var offset = r.eo;
    if (target.tagName && target.childNodes[offset]) {
        target = target.childNodes[offset];
        offset = 0;
    }

    var node = dom.node(target);
    var data = dom.merge(node, target, offset, target, offset, null, true);
    data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
    r = range.create(data.sc, data.so);
    r.select();
    target = r.sc;
    offset = r.so;

    while (!dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}

    var contentAfter = target.textContent.slice(offset,Infinity).match(/\S|\u00A0/);
    var content = target.textContent.replace(/[ \t\r\n]+$/, '');
    var temp;
    var temp2;
    var next;

    // media
    if (dom.isImg(node) || (!contentAfter && dom.isImg(dom.hasContentAfter(node)))) {
        var parent;
        var index;
        if (!dom.isImg(node)) {
            node = dom.hasContentAfter(node);
        }
        while (dom.isImg(node)) {
            parent = node.parentNode;
            index = dom.position(node);
            if (index>0) {
                next = node.previousSibling;
                r = range.create(next, next.textContent.length);
            } else {
                r = range.create(parent, 0);
            }
            if (!dom.hasContentAfter(node) && !dom.hasContentBefore(node)) {
                parent.appendChild($('<br/>')[0]);
            }
            parent.removeChild(node);
            node = parent;
            r.select();
        }
    }
    // empty tag
    else if (!content.length && target.tagName && dom.isRemovableEmptyNode(dom.isBR(target) ? target.parentNode : target)) {
        if (node === $editable[0] || $.contains(node, $editable[0])) {
            event.preventDefault();
            return false;
        }
        var before = false;
        next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
        if (!dom.isContentEditable(next)) {
            before = true;
            next = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
        }
        dom.removeSpace(next.parentNode, next, 0, next, 0); // clean before jump for not select invisible space between 2 tag
        next = dom.firstChild(next);
        node.parentNode.removeChild(node);
        range.create(next, before ? next.textContent.length : 0).select();
    }
    // normal feature if same tag and not the end
    else if (contentAfter) {
        return true;
    }
    // merge with the next text node
    else if (dom.isText(target) && (temp = dom.hasContentAfter(target)) && dom.isText(temp)) {
        return true;
    }
    //merge with the next block
    else if ((temp = dom.ancestorHaveNextSibling(target)) &&
            !r.isOnCell() &&
            dom.isMergable(temp) &&
            dom.isMergable(temp2 = dom.hasContentAfter(temp)) &&
            temp.tagName === temp2.tagName &&
            (temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
            !dom.isNotBreakable(temp) &&
            !dom.isNotBreakable(temp2)) {
        dom.autoMerge(target, false);
        next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
        if (dom.isBR(next)) {
            if (dom.position(next) === 0) {
                range.create(next.parentNode, 0).select();
            }
            else {
                range.create(next.previousSibling, next.previousSibling.textContent.length).select();
            }
            next.parentNode.removeChild(next);
        } else {
            range.create(next, 0).select();
        }
    }
    // jump to next node for delete
    else if ((temp = dom.ancestorHaveNextSibling(target)) && (temp2 = dom.hasContentAfter(temp)) && dom.isContentEditable(temp2)) {

        dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
        temp2 = dom.firstChild(temp2);

        r = range.create(temp2, 0);
        r.select();

        if ((dom.isText(temp) || dom.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || dom.getComputedStyle(temp2).display === "inline")) {
            if (dom.isText(temp2)) {
                temp2.textContent = temp2.textContent.replace(/^\s*\S/, '');
            } else {
                $.summernote.pluginEvents.delete(event, editor, layoutInfo);
            }
        }
    }

    $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
    event.preventDefault();
    return false;
};
$.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable, "backspace");

    var r = range.create();
    if (!r) return;
    if (!r.isContentEditable()) {
        event.preventDefault();
        return false;
    }
    if (!r.isCollapsed()) {
        if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
            remove_table_content(r);
            range.create(r.sc, dom.nodeLength(r.sc)).select();
        } else {
            r = r.deleteContents();
            r.select();
        }
        event.preventDefault();
        return false;
    }

    var target = r.sc;
    var offset = r.so;
    if (target.tagName && target.childNodes[offset]) {
        target = target.childNodes[offset];
        offset = 0;
    }

    var node = dom.node(target);
    var data = dom.merge(node, target, offset, target, offset, null, true);
    data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
    r = dom.isVoid(data.sc) ? range.createFromNode(data.sc) : range.create(data.sc, data.so);
    r.select();
    target = r.sc;
    offset = r.so;
    if (target.tagName && target.childNodes[offset]) {
        target = target.childNodes[offset];
        offset = 0;
        node = dom.node(target);
    }

    while (node.parentNode && !dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}

    var contentBefore = target.textContent.slice(0,offset).match(/\S|\u00A0/);
    var content = target.textContent.replace(/[ \t\r\n]+$/, '');
    var temp;
    var temp2;
    var prev;

    // delete media
    if (dom.isImg(node) || (!contentBefore && dom.isImg(dom.hasContentBefore(node)))) {
        if (!dom.isImg(node)) {
            node = dom.hasContentBefore(node);
        }
        range.createFromNode(node).select();
        $.summernote.pluginEvents.delete(event, editor, layoutInfo);
    }
    // table tr td
    else if (r.isOnCell() && !offset && (target === (temp = dom.ancestor(target, dom.isCell)) || target === temp.firstChild || (dom.isText(temp.firstChild) && !dom.isVisibleText(temp.firstChild) && target === temp.firstChild.nextSibling))) {
        if (dom.previousElementSibling(temp)) {
            var td = dom.previousElementSibling(temp);
            node = td.lastChild || td;
        } else {
            var tr = temp.parentNode;
            var prevTr = dom.previousElementSibling(tr);
            if (!$(temp.parentNode).text().match(/\S|\u00A0/)) {
                if (prevTr) {
                    node = dom.lastChild(dom.lastElementChild(prevTr));
                } else {
                    node = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(tr)) || $editable.get(0));
                }
                $(tr).empty();
                if (!$(tr).closest('table').has('td, th').length) {
                    $(tr).closest('table').remove();
                }
                $(tr).remove();
                range.create(node, node.textContent.length, node, node.textContent.length).select();
            } else {
                node = dom.lastElementChild(prevTr).lastChild || dom.lastElementChild(prevTr);
            }
        }
        if (dom.isBR(node)) {
            range.createFromNode(node).select();
        } else {
            range.create(node, dom.nodeLength(node)).select();
        }
    }
    // empty tag
    else if (!content.length && target.tagName && dom.isRemovableEmptyNode(target)) {
        if (node === $editable[0] || $.contains(node, $editable[0])) {
            event.preventDefault();
            return false;
        }
        var before = true;
        prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
        if (!dom.isContentEditable(prev)) {
            before = false;
            prev = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
        }
        dom.removeSpace(prev.parentNode, prev, 0, prev, 0); // clean before jump for not select invisible space between 2 tag
        prev = dom.lastChild(prev);
        node.parentNode.removeChild(node);
        range.createFromNode(prev).select();
        range.create(prev, before ? prev.textContent.length : 0).select();
    }
    // normal feature if same tag and not the begin
    else if (contentBefore) {
        return true;
    }
    // merge with the previous text node
    else if (dom.isText(target) && (temp = dom.hasContentBefore(target)) && (dom.isText(temp) || dom.isBR(temp))) {
        return true;
    }
    //merge with the previous block
    else if ((temp = dom.ancestorHavePreviousSibling(target)) &&
            dom.isMergable(temp) &&
            dom.isMergable(temp2 = dom.hasContentBefore(temp)) &&
            temp.tagName === temp2.tagName &&
            (temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
            !dom.isNotBreakable(temp) &&
            !dom.isNotBreakable(temp2)) {
        prev = dom.firstChild(target);
        dom.autoMerge(target, true);
        range.create(prev, 0).select();
    }
    // jump to previous node for delete
    else if ((temp = dom.ancestorHavePreviousSibling(target)) && (temp2 = dom.hasContentBefore(temp)) && dom.isContentEditable(temp2)) {

        dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
        temp2 = dom.lastChild(temp2);

        r = range.create(temp2, temp2.textContent.length, temp2, temp2.textContent.length);
        r.select();

        if ((dom.isText(temp) || dom.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || dom.getComputedStyle(temp2).display === "inline")) {
            if (dom.isText(temp2)) {
                temp2.textContent = temp2.textContent.replace(/\S\s*$/, '');
            } else {
                $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
            }
        }
    }

    r = range.create();
    if (r) {
        $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
        dom.scrollIntoViewIfNeeded(r.sc.parentNode.previousElementSibling || r.sc);
    }

    event.preventDefault();
    return false;
};

function isFormatNode(node) {
    return node.tagName && options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
}

$.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutInfo, sorted) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable);

    var parent;
    var r = range.create();
    if (!r) return;
    var node = r.sc;
    while (node && node !== $editable[0]) {

        parent = node.parentNode;
        if (node.tagName === (sorted ? "UL" : "OL")) {

            var ul = document.createElement(sorted ? "ol" : "ul");
            ul.className = node.className;
            parent.insertBefore(ul, node);
            while (node.firstChild) {
                ul.appendChild(node.firstChild);
            }
            parent.removeChild(node);
            r.select();
            return;

        } else if (node.tagName === (sorted ? "OL" : "UL")) {

            var lis = [];
            for (var i=0; i<node.children.length; i++) {
                lis.push(node.children[i]);
            }

            if (parent.tagName === "LI") {
                node = parent;
                parent = node.parentNode;
                _.each(lis, function (li) {
                    parent.insertBefore(li, node);
                });
            } else {
                _.each(lis, function (li) {
                    while (li.firstChild) {
                        parent.insertBefore(li.firstChild, node);
                    }
                });
            }

            parent.removeChild(node);
            r.select();
            return;

        }
        node = parent;
    }

    var p0 = r.sc;
    while (p0 && p0.parentNode && p0.parentNode !== $editable[0] && !isFormatNode(p0)) {
        p0 = p0.parentNode;
    }
    if (!p0) return;
    var p1 = r.ec;
    while (p1 && p1.parentNode && p1.parentNode !== $editable[0] && !isFormatNode(p1)) {
        p1 = p1.parentNode;
    }
    if (!p0.parentNode || p0.parentNode !== p1.parentNode) {
        return;
    }

    parent = p0.parentNode;
    ul = document.createElement(sorted ? "ol" : "ul");
    parent.insertBefore(ul, p0);
    var childNodes = parent.childNodes;
    var brs = [];
    var begin = false;
    for (i = 0; i < childNodes.length; i++) {
        if (begin && dom.isBR(childNodes[i])) {
            parent.removeChild(childNodes[i]);
            i--;
        }
        if ((!dom.isText(childNodes[i]) && !isFormatNode(childNodes[i])) || (!ul.firstChild && childNodes[i] !== p0) ||
            $.contains(ul, childNodes[i]) || (dom.isText(childNodes[i]) && !childNodes[i].textContent.match(/\S|u00A0/))) {
            continue;
        }
        begin = true;
        var li = document.createElement('li');
        ul.appendChild(li);
        li.appendChild(childNodes[i]);
        if (li.firstChild === p1) {
            break;
        }
        i--;
    }
    if (dom.isBR(childNodes[i])) {
        parent.removeChild(childNodes[i]);
    }

    for (i = 0; i < brs.length ; i++) {
        parent.removeChild(brs[i]);
    }
    r.clean().select();
    event.preventDefault();
    return false;
};
$.summernote.pluginEvents.insertOrderedList = function (event, editor, layoutInfo) {
    return $.summernote.pluginEvents.insertUnorderedList(event, editor, layoutInfo, true);
};
$.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable);
    var r = range.create();
    if (!r) return;

    var flag = false;
    function indentUL(UL, start, end) {
        var next;
        var tagName = UL.tagName;
        var node = UL.firstChild;
        var ul = document.createElement(tagName);
        var li = document.createElement("li");
        li.style.listStyle = "none";
        li.appendChild(ul);

        if (flag) {
            flag = 1;
        }

        // create and fill ul into a li
        while (node) {
            if (flag === 1 || node === start || $.contains(node, start)) {
                flag = true;
                node.parentNode.insertBefore(li, node);
            }
            next = dom.nextElementSibling(node);
            if (flag) {
                ul.appendChild(node);
            }
            if (node === end || $.contains(node, end)) {
                flag = false;
                break;
            }
            node = next;
        }

        var temp;
        var prev = dom.previousElementSibling(li);
        if (prev && prev.tagName === "LI" && (temp = dom.firstElementChild(prev)) && temp.tagName === tagName && ((dom.firstElementChild(prev) || prev.firstChild) !== ul)) {
            dom.doMerge(dom.firstElementChild(prev) || prev.firstChild, ul);
            li = prev;
            li.parentNode.removeChild(dom.nextElementSibling(li));
        }
        next = dom.nextElementSibling(li);
        if (next && next.tagName === "LI" && (temp = dom.firstElementChild(next)) && temp.tagName === tagName && (dom.firstElementChild(li) !== dom.firstElementChild(next))) {
            dom.doMerge(dom.firstElementChild(li), dom.firstElementChild(next));
            li.parentNode.removeChild(dom.nextElementSibling(li));
        }
    }
    function outdenttUL(UL, start, end) {
        var next;
        var node = UL.firstChild;
        var parent = UL.parentNode;
        var li = UL.parentNode.tagName === "LI" ? UL.parentNode : UL;
        var ul = UL.parentNode.tagName === "LI" ? UL.parentNode.parentNode : UL.parentNode;
        start = dom.ancestor(start, dom.isLi);
        end = dom.ancestor(end, dom.isLi);

        if (ul.tagName !== "UL" && ul.tagName !== "OL") return;

        // create and fill ul into a li
        while (node) {
            if (node === start || $.contains(node, start)) {
                flag = true;
                if (dom.previousElementSibling(node) && li.tagName === "LI") {
                    li = dom.splitTree(li, dom.prevPoint({'node': node, 'offset': 0}));
                }
            }
            next = dom.nextElementSibling(node);
            if (flag) {
                ul = node.parentNode;
                li.parentNode.insertBefore(node, li);
                if (!ul.children.length) {
                    if (ul.parentNode.tagName === "LI") {
                        ul = ul.parentNode;
                    }
                    ul.parentNode.removeChild(ul);
                }
            }

            if (node === end || $.contains(node, end)) {
                flag = false;
                break;
            }
            node = next;
        }

        dom.merge(parent, start, 0, end, 1, null, true);
    }
    function indentOther(p, start, end) {
        if (p === start || $.contains(p, start) || $.contains(start, p)) {
            flag = true;
        }
        if (flag) {
            if (outdent) {
                dom.outdent(p);
            } else {
                dom.indent(p);
            }
        }
        if (p === end || $.contains(p, end) || $.contains(end, p)) {
            flag = false;
        }
    }

    var ancestor = r.commonAncestor();
    var $dom = $(ancestor);

    if (!dom.isList(ancestor)) {
        // to indent a selection, we indent the child nodes of the common
        // ancestor that contains this selection
        $dom = $(dom.node(ancestor)).children();
    }
    if (!$dom.not('br').length) {
        // if selection is inside a list, we indent its list items
        $dom = $(dom.ancestor(r.sc, dom.isList));
        if (!$dom.length) {
            // if the selection is contained in a single HTML node, we indent
            // the first ancestor 'content block' (P, H1, PRE, ...) or TD
            $dom = $(r.sc).closest(options.styleTags.join(',')+',td');
        }
    }

    // if select tr, take the first td
    $dom = $dom.map(function () { return this.tagName === "TR" ? dom.firstElementChild(this) : this; });

    $dom.each(function () {
        if (flag || $.contains(this, r.sc)) {
            if (dom.isList(this)) {
                if (outdent) {
                    outdenttUL(this, r.sc, r.ec);
                } else {
                    indentUL(this, r.sc, r.ec);
                }
            } else if (isFormatNode(this) || dom.ancestor(this, dom.isCell)) {
                indentOther(this, r.sc, r.ec);
            }
        }
    });

    if ($dom.length) {
        var $parent = $dom.parent();

        // remove text nodes between lists
        var $ul = $parent.find('ul, ol');
        if (!$ul.length) {
            $ul = $(dom.ancestor(r.sc, dom.isList));
        }
        $ul.each(function () {
            if (this.previousSibling &&
                this.previousSibling !== dom.previousElementSibling(this) &&
                !this.previousSibling.textContent.match(/\S/)) {
                this.parentNode.removeChild(this.previousSibling);
            }
            if (this.nextSibling &&
                this.nextSibling !== dom.nextElementSibling(this) &&
                !this.nextSibling.textContent.match(/\S/)) {
                this.parentNode.removeChild(this.nextSibling);
            }
        });

        // merge same ul or ol
        r = dom.merge($parent[0], r.sc, r.so, r.ec, r.eo, function (prev, cur) {
                if (prev && dom.isList(prev) && dom.isEqual(prev, cur)) {
                    return true;
                }
            }, true);
        range.create(r.sc, r.so, r.ec, r.eo).select();
    }
    event.preventDefault();
    return false;
};
$.summernote.pluginEvents.outdent = function (event, editor, layoutInfo) {
    return $.summernote.pluginEvents.indent(event, editor, layoutInfo, true);
};

$.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTagName) {
    $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, "Default");
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable);
    event.preventDefault();

    var r = range.create();
    if (!r) {
        return;
    }
    // select content since container (that firefox selects) may be removed
    if (r.so === 0) {
        r.sc = dom.firstChild(r.sc);
    }
    if (dom.nodeLength(r.ec) >= r.eo) {
        r.ec = dom.lastChild(r.ec);
        r.eo = dom.nodeLength(r.ec);
    }
    r = range.create(r.sc, r.so, r.ec, r.eo);
    r.reRange().select();

    if (sTagName === "blockquote" || sTagName === "pre") {
      sTagName = $.summernote.core.agent.isMSIE ? '<' + sTagName + '>' : sTagName;
      document.execCommand('FormatBlock', false, sTagName);
      return;
    }

    // fix by odoo because if you select a style in a li with no p tag all the ul is wrapped by the style tag
    var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
    for (var i=0; i<nodes.length; i++) {
        if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && dom.isVisibleText(nodes[i])) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
            var ancestor = dom.ancestor(nodes[i], isFormatNode);
            if ($(ancestor).parent().is('blockquote')) {
                // firefox may wrap formatting block in blockquote
                $(ancestor).unwrap();
            }
            if (!ancestor) {
                dom.wrap(nodes[i], sTagName);
            } else if (ancestor.tagName.toLowerCase() !== sTagName) {
                var tag = document.createElement(sTagName);
                ancestor.parentNode.insertBefore(tag, ancestor);
                dom.moveContent(ancestor, tag);
                if (ancestor.className) {
                    tag.className = ancestor.className;
                }
                ancestor.parentNode.removeChild(ancestor);
            }
        }
    }
    r.select();
};
$.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, value) {
    var $editable = layoutInfo.editable();
    $editable.data('NoteHistory').recordUndo($editable);
    var r = range.create();
    if (!r) return;
    var node = range.create().sc.parentNode;
    document.execCommand('removeFormat');
    document.execCommand('removeFormat');
    r = range.create();
    if (!r) return;
    r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
    range.create(r.sc, r.so, r.ec, r.eo).select();
    event.preventDefault();
    return false;
};

eventHandler.modules.editor.undo = function ($popover) {
    if (!$popover.attr('disabled')) $popover.data('NoteHistory').undo();
};
eventHandler.modules.editor.redo = function ($popover) {
    if (!$popover.attr('disabled'))  $popover.data('NoteHistory').redo();
};

// Get color and background color of node to update recent color button
var fn_from_node = eventHandler.modules.editor.style.fromNode;
eventHandler.modules.editor.style.fromNode = function ($node) {
    var styleInfo = fn_from_node.apply(this, arguments);
    styleInfo['color'] = $node.css('color');
    styleInfo['background-color'] = $node.css('background-color');
    return styleInfo;
};

// use image toolbar if current range is on image
var fn_editor_currentstyle = eventHandler.modules.editor.currentStyle;
eventHandler.modules.editor.currentStyle = function (target) {
    var styleInfo = fn_editor_currentstyle.apply(this, arguments);
    // with our changes for inline editor, the targeted element could be a button of the editor
    if (!styleInfo.image || !dom.isEditable(styleInfo.image)) {
        styleInfo.image = undefined;
        var r = range.create();
        if (r)
            styleInfo.image = r.isOnImg();
    }
    // Fix when the target is a link: the text-align buttons state should
    // indicate the alignment of the link in the parent, not the text inside
    // the link (which is not possible to customize with summernote). Summernote fixed
    // this in their newest version... by just not showing the active button
    // for alignments.
    if (styleInfo.anchor) {
        styleInfo['text-align'] = $(styleInfo.anchor).parent().css('text-align');
    }
    return styleInfo;
};

options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62];
$.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) {
    var r = range.create();
    if (!r) return;
    var startPoint = r.getStartPoint();
    var endPoint = r.getEndPoint();

    if (r.isCollapsed() && !dom.isFont(r.sc)) {
        return {
            sc: startPoint.node,
            so: startPoint.offset,
            ec: endPoint.node,
            offset: endPoint.offset
        };
    }

    if (startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset]) {
        startPoint.node = startPoint.node.childNodes[startPoint.offset];
        startPoint.offset = 0;
    }
    if (endPoint.node.tagName && endPoint.node.childNodes[endPoint.offset]) {
        endPoint.node = endPoint.node.childNodes[endPoint.offset];
        endPoint.offset = 0;
    }

    // get first and last point
    var ancestor;
    var node;
    if (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
      ancestor = dom.ancestor(endPoint.node, dom.isFont) || endPoint.node;
      dom.splitTree(ancestor, endPoint);
    }
    if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
      ancestor = dom.ancestor(startPoint.node, dom.isFont) || startPoint.node;
      node = dom.splitTree(ancestor, startPoint);
      if (endPoint.node === startPoint.node) {
        endPoint.node = node;
        endPoint.offset = dom.nodeLength(node);
      }
      startPoint.node = node;
      startPoint.offset = 0;
    }

    // get list of nodes to change
    var nodes = [];
    dom.walkPoint(startPoint, endPoint, function (point) {
      var node = point.node;
      if (((dom.isText(node) && dom.isVisibleText(node)) ||
          (dom.isFont(node) && !dom.isVisibleText(node))) &&
          (node !== endPoint.node || endPoint.offset)) {

          nodes.push(point.node);

      }
    });
    nodes = list.unique(nodes);

        // If ico fa
    if (r.isCollapsed()) {
        nodes.push(startPoint.node);
    }

    // apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
    var font, $font, fonts = [], className;
    var i;
    if (color || bgcolor || size) {
      for (i=0; i<nodes.length; i++) {
        node = nodes[i];

        font = dom.ancestor(node, dom.isFont);
        if (!font) {
          if (node.textContent.match(/^[ ]|[ ]$/)) {
            node.textContent = node.textContent.replace(/^[ ]|[ ]$/g, '\u00A0');
          }

          font = dom.create("font");
          node.parentNode.insertBefore(font, node);
          font.appendChild(node);
        }

        fonts.push(font);

        className = font.className.split(/\s+/);

        var k;
        if (color) {
          for (k=0; k<className.length; k++) {
            if (className[k].length && className[k].slice(0,5) === "text-") {
              className.splice(k,1);
              k--;
            }
          }

          if (color.indexOf('text-') !== -1) {
            font.className = className.join(" ") + " " + color;
            font.style.color = "inherit";
          } else {
            font.className = className.join(" ");
            font.style.color = color;
          }
        }
        if (bgcolor) {
          for (k=0; k<className.length; k++) {
            if (className[k].length && className[k].slice(0,3) === "bg-") {
              className.splice(k,1);
              k--;
            }
          }

          if (bgcolor.indexOf('bg-') !== -1) {
            font.className = className.join(" ") + " " + bgcolor;
            font.style.backgroundColor = "inherit";
          } else {
            font.className = className.join(" ");
            font.style.backgroundColor = bgcolor;
          }
        }
        if (size) {
          font.style.fontSize = "inherit";
          if (!isNaN(size) && Math.abs(parseInt(dom.getComputedStyle(font).fontSize, 10)-size)/size > 0.05) {
            font.style.fontSize = size + "px";
          }
        }
      }
    }

    // remove empty values
    // we must remove the value in 2 steps (applay inherit then remove) because some
    // browser like chrome have some time an error for the rendering and/or keep inherit
    for (i=0; i<fonts.length; i++) {
        font = fonts[i];
        if (font.style.backgroundColor === "inherit") {
            font.style.backgroundColor = "";
        }
        if (font.style.color === "inherit") {
            font.style.color = "";
        }
        if (font.style.fontSize === "inherit") {
            font.style.fontSize = "";
        }

        $font = $(font);

        if (!$font.css("color") && !$font.css("background-color") && !$font.css("font-size")) {
            $font.removeAttr("style");
        }
        if (!font.className.length) {
            $font.removeAttr("class");
        }
    }

    // select nodes to clean (to remove empty font and merge same nodes)
    nodes = [];
    dom.walkPoint(startPoint, endPoint, function (point) {
      nodes.push(point.node);
    });
    nodes = list.unique(nodes);

    function remove(node, to) {
      if (node === endPoint.node) {
        endPoint = dom.prevPoint(endPoint);
      }
      if (to) {
        dom.moveContent(node, to);
      }
      dom.remove(node);
    }

    // remove node without attributes (move content), and merge the same nodes
     var className2, style, style2;
     for (i=0; i<nodes.length; i++) {
      node = nodes[i];

      if ((dom.isText(node) || dom.isBR(node)) && !dom.isVisibleText(node)) {
        remove(node);
        nodes.splice(i,1);
        i--;
        continue;
      }

      font = dom.ancestor(node, dom.isFont);
      node = font || dom.ancestor(node, dom.isSpan);

      if (!node) {
        continue;
      }

      $font = $(node);
      className = dom.orderClass(node);
      style = dom.orderStyle(node);

      if (!className && !style) {
        remove(node, node.parentNode);
        nodes.splice(i,1);
        i--;
        continue;
      }

      if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
        className2 = font.getAttribute('class');
        style2 = font.getAttribute('style');
        if (node !== font && className === className2 && style === style2) {
          remove(node, font);
          nodes.splice(i,1);
          i--;
          continue;
        }
      }
     }

    range.create(startPoint.node, startPoint.offset, endPoint.node, endPoint.offset).select();
};
$.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value) {
  var $editable = layoutInfo.editable();
  event.preventDefault();
  $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, value);
  editor.afterCommand($editable);
};
$.summernote.pluginEvents.color = function (event, editor, layoutInfo, sObjColor) {
  var oColor = JSON.parse(sObjColor);
  var foreColor = oColor.foreColor, backColor = oColor.backColor;

  if (foreColor) { $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, foreColor); }
  if (backColor) { $.summernote.pluginEvents.backColor(event, editor, layoutInfo, backColor); }
};
$.summernote.pluginEvents.foreColor = function (event, editor, layoutInfo, foreColor) {
  var $editable = layoutInfo.editable();
  $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, foreColor, null, null);
  editor.afterCommand($editable);
};
$.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backColor) {
  var $editable = layoutInfo.editable();
  var r = range.create();
  if (!r) return;
  if (r.isCollapsed() && r.isOnCell()) {
    var cell = dom.ancestor(r.sc, dom.isCell);
    cell.className = cell.className.replace(new RegExp('(^|\\s+)bg-[^\\s]+(\\s+|$)', 'gi'), '');
    cell.style.backgroundColor = "";
    if (backColor.indexOf('bg-') !== -1) {
      cell.className += ' ' + backColor;
    } else if (backColor !== 'inherit') {
      cell.style.backgroundColor = backColor;
    }
    return;
  }
  $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, backColor, null);
  editor.afterCommand($editable);
};

options.onCreateLink = function (sLinkUrl) {
    if (sLinkUrl.indexOf('mailto:') === 0 || sLinkUrl.indexOf('tel:') === 0) {
      sLinkUrl = sLinkUrl.replace(/^tel:([0-9]+)$/, 'tel://$1');
    } else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
      sLinkUrl =  'mailto:' + sLinkUrl;
    } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl[0] !== '/'
               && sLinkUrl[0] !== '#' && sLinkUrl.slice(0, 2) !== '${') {
      sLinkUrl = 'http://' + sLinkUrl;
    }
    return sLinkUrl;
};

function summernote_table_scroll(event) {
    var r = range.create();
    if (r && r.isOnCell()) {
        $('.o_table_handler').remove();
    }
}
function summernote_table_update(oStyle) {
    var r = range.create();
    if (!oStyle.range || !r || !r.isOnCell() || !r.isOnCellFirst()) {
        $('.o_table_handler').remove();
        return;
    }
    var table = dom.ancestor(oStyle.range.sc, dom.isTable);
    if (!table) { // if the editable tag is inside the table
        return;
    }
    var $editable = $(table).closest('.o_editable');

    $('.o_table_handler').remove();

    var $dels = $();
    var $adds = $();
    var $tds = $('tr:first', table).children();
    $tds.each(function () {
        var $td = $(this);
        var pos = $td.offset();

        var $del = $('<span class="o_table_handler fa fa-minus-square"/>').appendTo('body');
        $del.data('td', this);
        $dels = $dels.add($del);
        $del.css({
            left: ((pos.left + $td.outerWidth()/2)-6) + "px",
            top: (pos.top-6) + "px"
        });

        var $add = $('<span class="o_table_handler fa fa-plus-square"/>').appendTo('body');
        $add.data('td', this);
        $adds = $adds.add($add);
        $add.css({
            left: (pos.left-6) + "px",
            top: (pos.top-6) + "px"
        });
    });

    var $last = $tds.last();
    var pos = $last.offset();
    var $add = $('<span class="o_table_handler fa fa-plus-square"/>').appendTo('body');
    $adds = $adds.add($add);
    $add.css({
        left: (pos.left+$last.outerWidth()-6) + "px",
        top: (pos.top-6) + "px"
    });

    var $table = $(table);
    $dels.data('table', table).on('mousedown', function (event) {
        var td = $(this).data('td');
        $editable.data('NoteHistory').recordUndo($editable);

        var newTd;
        if ($(td).siblings().length) {
            var eq = $(td).index();
            $table.find('tr').each(function () {
                $('> td:eq('+eq+')', this).remove();
            });
            newTd = $table.find('tr:first > td:eq('+eq+'), tr:first > td:last').first();
        } else {
            var prev = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling($table[0])));
            $table.remove();
            $('.o_table_handler').remove();
            r = range.create(prev, prev.textContent.length);
            r.select();
            $(r.sc).trigger('mouseup');
            return;
        }

        $('.o_table_handler').remove();
        range.create(newTd[0], 0, newTd[0], 0).select();
        newTd.trigger('mouseup');
    });
    $adds.data('table', table).on('mousedown', function (event) {
        var td = $(this).data('td');
        $editable.data('NoteHistory').recordUndo($editable);

        var newTd;
        if (td) {
            var eq = $(td).index();
            $table.find('tr').each(function () {
                $('td:eq('+eq+')', this).before('<td>'+dom.blank+'</td>');
            });
            newTd = $table.find('tr:first td:eq('+eq+')');
        } else {
            $table.find('tr').each(function () {
                $(this).append('<td>'+dom.blank+'</td>');
            });
            newTd = $table.find('tr:first td:last');
        }

        $('.o_table_handler').remove();
        range.create(newTd[0], 0, newTd[0], 0).select();
        newTd.trigger('mouseup');
    });

    $dels.css({
        'position': 'absolute',
        'cursor': 'pointer',
        'background-color': '#fff',
        'color': '#ff0000'
    });
    $adds.css({
        'position': 'absolute',
        'cursor': 'pointer',
        'background-color': '#fff',
        'color': '#00ff00'
    });
}
var fn_popover_update = eventHandler.modules.popover.update;
eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
    fn_popover_update.call(this, $popover, oStyle, isAirMode);
    if ((isAirMode ? $popover : $popover.parent()).find('.note-table').length) {
        summernote_table_update(oStyle);
    }
};

var fn_attach = eventHandler.attach;
eventHandler.attach = function (oLayoutInfo, options) {
    var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
    fn_attach.call(this, oLayoutInfo, options);
    $editable.on("scroll", summernote_table_scroll);
};
var fn_detach = eventHandler.detach;
eventHandler.detach = function (oLayoutInfo, options) {
    var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
    fn_detach.call(this, oLayoutInfo, options);
    $editable.off("scroll", summernote_table_scroll);
    $('.o_table_handler').remove();
};

options.icons.image.image = "file-image-o";
$.summernote.lang['en-US'].image.image = "File / Image";

return $.summernote;
});
