/* <copyright>
 This file contains proprietary software owned by Motorola Mobility, Inc.<br/>
 No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/>
 (c) Copyright 2011 Motorola Mobility, Inc.  All Rights Reserved.
 </copyright> */
/**
	@module "montage/ui/rich-text-editor.reel"
    @requires montage/core/core
*/
var Montage = require("montage/core/core").Montage,
    Component = require("montage/ui/component").Component,
    MutableEvent = require("montage/core/event/mutable-event").MutableEvent,
    Resizer = require("node_modules/labs/rich-text-editor.reel/rich-text-resizer").Resizer,
    Sanitizer = require("node_modules/labs/rich-text-editor.reel/rich-text-sanitizer").Sanitizer;
    Point = require("montage/core/geometry/point").Point;


/**
    @class module:"montage/ui/rich-text-editor.reel".RichTextEditor
    @extends module:montage/ui/component.Component
*/
exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/rich-text-editor.reel".RichTextEditor# */ {

    /**
      Description TODO
      @private
    */
    _hasSelectionChangeEvent: {
        enumerable: false,
        value: null     // Need to be preset to null, will be set to true or false later on
    },

    /**
      Description TODO
      @private
    */
    _uniqueId: {
        enumerable: false,
        value: Math.floor(Math.random() * 1000) + "-" + Math.floor(Math.random() * 1000)
    },

    /**
      Description TODO
      @private
    */
    _needsSelectionReset: {
        enumerable: false,
        value: false
    },

    /**
      Description TODO
      @private
    */
    _selectionChangeTimer: {
        enumerable: false,
        value: null
    },

    /**
      Description TODO
      @private
    */
    _activeLink: {
        enumerable: false,
        value: null
    },

    /**
      Description TODO
      @private
    */
    _needsActiveLinkOn: {
        enumerable: false,
        value: false
    },

    /**
      Description TODO
      @private
    */
    _hasFocus: {
        enumerable: false,
        value: false
    },

    /**
      Description TODO
     @type {Function}
    */
    hasFocus: {
        enumerable: true,
        get: function() {
            return this._hasFocus;
        }
    },

    /**
      Description TODO
      @private
    */
    _dirty: {
        enumerable: false,
        value: false
    },

    /**
      Description TODO
      @private
    */
    _value: {
        enumerable: false,
        value: ""
    },

    /**
      Description TODO
     @type {Function}
    */
    value: {
        enumerable: true,
        serializable: true,
        get: function() {
            var contentNode = this.element.firstChild,
                content;

            if (this._dirtyValue) {
                if (this._resizer) {
                    contentNode = this._resizer.cleanup(contentNode);
                }

                contentNode = this._cleanupActiveLink(contentNode);

                content = contentNode ? contentNode.innerHTML : "";
                if (content == "<br>") {
                    // when the contentEditable div is emptied, Chrome add a <br>, let's filter it out
                    content = "";
                }
                if (this._sanitizer) {
                    content = this._sanitizer.didGetValue(content, this._uniqueId);
                }

                this._value = content;
                this._dirtyValue = false;
            }
            return this._value;
        },
        set: function(value) {
            if (this._value !== value || this._dirtyValue) {
                if (this._resizer) {
                    this._needsHideResizer = true;
                }
                if (this._sanitizer) {
                    value = this._sanitizer.willSetValue(value, this._uniqueId);
                }
                this._value = value;
                this._dirtyValue = false;
                this._dirtyTextValue = true;
                this._needsSelectionReset = true;
                this._needsResetContent = true;
                this.needsDraw = true;
            }
        }
    },

    /**
      Description TODO
      @private
    */
    _textValue: {
        enumerable: false,
        value: ""
    },

    /**
      Description TODO
     @type {Function}
    */
    textValue: {
        enumerable: true,
        get: function() {
            var contentNode = this.element.firstChild,
                childNodes;

            if (this._dirtyTextValue) {
                if (contentNode) {
                    if (this._resizer) {
                        contentNode = this._resizer.cleanup(contentNode);
                    }
                    contentNode = this._cleanupActiveLink(contentNode);
                }

                this._textValue = contentNode ? this._innerText(contentNode) : "";
                this._dirtyTextValue = false;
            }
            return this._textValue;
        },
        set: function (value) {
            if (this._textValue !== value || this._dirtyTextValue) {
                if (this._resizer) {
                    this._needsHideResizer = true;
                }

                this._textValue = value;
                this._dirtyTextValue = false;
                this._dirtyValue = true;
                this._needsSelectionReset = true;
                this._needsResetContent = true;
                this.needsDraw = true;
            }
        }
    },

    /**
      Description TODO
     @type {}
    */
    delegate: {
        enumerable: true,
        value: null
    },

    /**
      Description TODO
      @private
    */
    _sanitizer: {
        enumerable: false,
        value: Sanitizer.create()
    },

    /**
      Description TODO
     @type {Function}
    */
    sanitizer: {
        enumerable: false,
        get: function() {
            return  this._sanitizer;
        },
        set: function(value) {
            this._sanitizer = value;
        }
    },

    /**
      Description TODO
      @private
    */
    _resizer: {
        enumerable: false,
        value: Resizer.create()
    },

    /**
      Description TODO
     @type {Function}
    */
    resizer: {
        enumerable: false,
        get: function() {
            return  this._resizer;
        },
        set: function(value) {
            // force hide the current resizer
            if (this._resizer){
                this._resizer.hide(true);
                delete this._needsHideResizer;
            }
            this._resizer = value;
            this._resizer.initialize(this);
        }
    },

    /**
      Description TODO
      @private
    */
    _statesDirty: {
        enumerable: false,
        value: false
    },

    /**
      Description TODO
      @private
    */
    _states: {
        enumerable: false,
        value: null
    },

    /**
      Description TODO
     @type {Function}
    */
    states: {
        enumerable: true,
        get: function() {
            if (this._statesDirty || !this._states) {
                this.updateStates();
            }
            return this._states;
        }
    },

    /**
      Description TODO
     @type {Function}
    */
    updateStates: {
        enumerable: true,
        value: function() {
            var actions = this._actions,
                key,
                action,
                states,
                state,
                hasFocus = this._hasFocus;

            if (this._states == null || this._statesDirty) {
                this._states = this._states || {};

                if (hasFocus) {
                    this._statesDirty = false;
                    states = this._states;
                    for (key in actions) {
                        action = actions[key];
                        state = "false";
                        if (action.enabled && action.status) {
                            state = document.queryCommandValue(key);
                            if (typeof state == "boolean") {
                                state = state ? "true" : "false";
                            }

                            // Clean up font name
                            if (key == "fontname") {
                                state = state.replace(/'/g, "");
                            }
                        }

                        if (states[key] !== state) {
                            states[key] = state;
                        }
                    }
                }
            }

            return this._states;
        }
    },

    /**
      Description TODO
      @private
    */
    _allowDrop: {
        enumerable: false,
        value: true
    },

    /**
      Description TODO
     @type {Function}
    */
    allowDrop: {
        enumerable: true,
        serializable: true,
        get: function() {
            return this._allowDrop;
        },
        set: function(value) {
            this._allowDrop = value;
        }
    },

    /**
      Description TODO
      @private
    */
    _actions: {
        enumerable: false,
        value: {
            bold: {enabled: true, needsValue:false, status: true},
            justifyleft: {enabled: true, needsValue:false, status: true},
            justifycenter: {enabled: true, needsValue:false, status: true},
            justifyright: {enabled: true, needsValue:false, status: true},
            justifyfull: {enabled: true, needsValue:false, status: true},
            italic: {enabled: true, needsValue:false, status: true},
            underline: {enabled: true, needsValue:false, status: true},
            strikethrough: {enabled: true, needsValue:false, status: true},
            indent: {enabled: true, needsValue:false, status: false},
            outdent: {enabled: true, needsValue:false, status: false},
            insertorderedlist: {enabled: true, needsValue:false, status: true},
            insertunorderedlist: {enabled: true, needsValue:false, status: true},
            fontname: {enabled: true, needsValue:true, status: true},
            fontsize: {enabled: true, needsValue:true, status: true},
            hilitecolor: {enabled: true, needsValue:true, status: true},
            forecolor: {enabled: true, needsValue:true, status: true}
        }
    },

    /**
      Description TODO
     @type {Function}
    */
    actions: {
        enumerable: false,
        get: function() {
            var actions = this._actions,
                action,
                actionsArray = [];

            for (action in actions) {
                actionsArray.push(action);
            }

            return actionsArray;
        }
    },

    /**
      Description TODO
     @type {Function}
    */
    enabledActions: {
        enumerable: true,
        serializable: true,
        get: function() {
            var actions = this._actions,
                action,
                actionsArray = [];

            for (action in actions) {
                if (actions[action].enabled) {
                    actionsArray.push(action);
                }
            }

            return actionsArray;
        },
        set: function(enabledActions) {
            var actions = this._actions,
                nbrEnabledActions = enabledActions.length,
                action,
                i;

            for (action in actions) {
                actions[action].enabled = false;
            }

            for (i = 0; i < nbrEnabledActions; i ++) {
                action = enabledActions[i];
                if (actions[action] !== undefined) {
                    actions[action].enabled = true;
                }
            }
        }
    },

    /**
      Description TODO
      @private
    */
    _needsFocus: {
        value: false
    },

    /**
      Description TODO
      @type {Function}
    */
    focus: {
        value: function() {
            this._needsFocus = true;
            this.needsDraw = true;
        }
    },

    // Component Callbacks
    /**
    Description TODO
    @function
    */
    prepareForDraw: {
        enumerable: false,
        value: function() {
            var el = this.element,
                div;

            if (this._resizer) {
                this._resizer.initialize(this);
            }
            el.classList.add('montage-editor-frame');

            el.addEventListener("focus", this);
            el.addEventListener("dragstart", this, false);
            el.addEventListener("dragend", this, false);
            el.addEventListener("dragover", this, false);
            el.addEventListener("drop", this, false);

            this._needsResetContent = true;
        }
    },

    /**
    Description TODO
    @function
    */
    draw: {
        enumerable: false,
        value: function() {
            var thisRef = this,
                editorElement = this.element,
                element,
                range,
                offset;

            if (this._needsResetContent === true) {
                // Reset the editor content in order to reset the browser undo stack
                editorElement.innerHTML = '<div class="montage-editor editor-' + this._uniqueId + '" contentEditable="true"></div>';

                // Set the contentEditable value
                if (this._value && !this._dirtyValue) {
                    editorElement.firstChild.innerHTML = this._value;
                    // Since this property affects the textValue, we need to fire a change event for it as well
                    this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("textValue" , this.textValue));
                } else if (this._textValue && !this._dirtyTextValue) {
                    editorElement.firstChild.innerText = this._textValue;
                    // Since this property affects the value, we need to fire a change event for it as well
                    this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("value" , this.value));
                } else {
                    editorElement.firstChild.innerHTML = "";
                }

                this._adjustPadding();
                this._markDirty();
                delete this._needsResetContent;
            }

            if (this._resizer) {
                // Need to hide the resizer?
                if (this._needsHideResizer) {
                    this._resizer.hide();
                    delete this._needsHideResizer;
                }

                // Need to show the resizer?
                if (this._needsShowResizerOn) {
                    element = this._needsShowResizerOn;
                    this._resizer.show(element);

                    // Select the element and its resizer
                    this._selectingResizer = true;
                    offset = this._nodeOffset(element);
                    range = document.createRange();
                    range.setStart(element.parentNode, offset);
                    range.setEnd(element.parentNode, offset + 1);
                    this._selectedRange = range;

                    // Note: Chrome (and maybe other browsers) will fire 2 selectionchange event asynchronously, to work around it let's use a timer
                    setTimeout(function() {delete thisRef._selectingResizer;}, 0);

                    delete this._needsShowResizerOn;
                }

                // Let's give a change to the resizer to do any custom drawing if needed
                this._resizer.draw();
            }

            if (this._needsActiveLinkOn !== false && this._needsActiveLinkOn != this._activeLink) {
                this._showActiveLink(this._needsActiveLinkOn);
                this._needsActiveLinkOn = false;
            }

        }
    },

    /**
    Description TODO
    @function
    */
    didDraw: {
        value: function() {
            if (this._needsFocus) {
                this.element.firstChild.focus();
                if(document.activeElement == this.element.firstChild) {
                    this._needsFocus = false;
                } else {
                    // Make sure the element is visible before trying again to set the focus
                    var style = window.getComputedStyle(this.element);
                    if (style.getPropertyValue("visibility") == "hidden" || style.getPropertyValue("display") == "none") {
                        this._needsFocus = false;
                    } else {
                        this.needsDraw = true;
                    }
                }
            }
        }
    },

    /**
    Description TODO
    @function
    */
    _adjustPadding: {
        enumerable: false,
        value: function() {
            var el = this.element.firstChild,
                minLeft = 0,
                minTop = 0;

            var walkTree = function(node, parentLeft, parentTop) {
                var nodes = node ? node.childNodes : [],
                    nbrNodes = nodes.length,
                    i,
                    offsetLeft = node.offsetLeft,
                    offsetTop = node.offsetTop;

                if (node.offsetParent) {
                    offsetLeft += parentLeft;
                    offsetTop += parentTop;
                }
                if (minLeft > offsetLeft) {
                    minLeft = offsetLeft;
                }
                if (minTop > offsetTop) {
                    minTop = offsetTop;
                }

                for (i = 0; i < nbrNodes; i ++) {
                    walkTree(nodes[i], offsetLeft, offsetTop)
                }
            };
            walkTree(el, el.offsetLeft, el.offsetTop);

            var computedStyle = document.defaultView.getComputedStyle(el),
                paddingLeft = computedStyle.paddingLeft,
                paddingTop = computedStyle.paddingTop;

            if (paddingLeft.match(/%$/)) {
                paddingLeft = parseInt(paddingLeft, 10) * el.clientWidth;
            } else {
                paddingLeft = parseInt(paddingLeft, 10);
            }
            if (paddingTop.match(/%$/)) {
                paddingTop = parseInt(paddingTop, 10) * el.clientHeight;
            } else {
                paddingTop = parseInt(paddingTop, 10);
            }

            if (minLeft < 0) {
                el.style.paddingLeft = (-minLeft - paddingLeft) + "px";
            }
            if (minTop < 0) {
                el.style.paddingTop = (-minTop - paddingTop) + "px";
            }
        }
    },

    // Event handlers
    // Event handlers
    /**
    Description TODO
    @function
    */
    handleFocus: {
        enumerable: false,
        value: function() {
            var thisRef = this,
                el = this.element,
                content = el.firstChild,
                savedRange,
                timer;

            this._hasFocus = true;
            if (this._needsSelectionReset) {
                var node = this._lastInnerNode(),
                    range,
                    length,
                    leafNodes = ["#text", "BR", "IMG"];

                // Select the last inner node
                if (node) {
                    if (leafNodes.indexOf(node.nodeName) !== -1) {
                        node = node.parentNode;
                    }
                    range = document.createRange();
                    length = node.childNodes ? node.childNodes.length : 0;
                    range.setStart(node, length);
                    range.setEnd(node, length);
                    this._selectedRange = range;
                }

                // Scroll the content to make sure the caret is visible, but only only if the focus wasn't the result of a user click/touch
                savedRange = this._selectedRange;
                timer = setInterval(function() {
                    if (thisRef._equalRange(thisRef._selectedRange, savedRange) &&
                            content.scrollTop + content.offsetHeight != content.scrollHeight) {
                        content.scrollTop = content.scrollHeight - content.offsetHeight;
                    }
                }, 10);

                setTimeout(function(){clearInterval(timer)}, 1000);

                this._needsSelectionReset = false;
            }

            el.addEventListener("blur", this);
            el.addEventListener("input", this);
            el.addEventListener("keypress", this);
            el.addEventListener("paste", this, false);
            el.addEventListener(window.Touch ? "touchstart" : "mousedown", this);
            document.addEventListener(window.Touch ? "touchend" : "mouseup", this);

            document.addEventListener("selectionchange", this, false);
            // Check if the browser does not supports the DOM event selectionchange
            if (this._hasSelectionChangeEvent === null) {
                var thisRef = this;
                setTimeout(function(){
                    if (thisRef._hasSelectionChangeEvent === null) {
                        thisRef._hasSelectionChangeEvent = false;
                    }
                }, 0);
            }
            if (this._hasSelectionChangeEvent === false) {
                // We need to listen to more event in order to simulate a selectionchange event
                el.addEventListener("keydup", this);
            }

            // Turn off image resize (if supported)
            document.execCommand("enableObjectResizing", false, false);
            // Force use css for styling (if supported)
            document.execCommand("styleWithCSS", false, true);

            // Update the states if they are dirty
            if (this._statesDirty) {
                this.updateStates();
            }
        }
    },

    /**
    Description TODO
    @function
    */
    handleBlur: {
        enumerable: false,
        value: function() {
            var el = this.element;

            // Force a selectionchange when we lose the focus
            this.handleSelectionchange();

            el.removeEventListener("blur", this);
            el.removeEventListener("input", this);
            el.removeEventListener("keypress", this);
            el.removeEventListener("paste", this, false);
            el.removeEventListener(window.Touch ? "touchstart" : "mousedown", this);
            document.removeEventListener(window.Touch ? "touchend" : "mouseup", this);

            document.removeEventListener("selectionchange", this);

            if (this._hasSelectionChangeEvent === false) {
                el.removeEventListener("keydup", this);
            }

            this._hasFocus = false;
        }
    },

    /**
    Description TODO
    @function
    */
    handleKeypress: {
        enumerable: false,
        value: function() {
            if (this._hasSelectionChangeEvent === false) {
                this.handleSelectionchange();
            }

            if (this._activeLink) {
                this._hideActiveLink();
            }

            this._markDirty();
        }
    },

    /**
    Description TODO
    @function
    */
    handleInput: {
        enumerable: false,
        value: function(event) {
            if (this._hasSelectionChangeEvent === false) {
                this.handleSelectionchange();
            }

            if (this._activeLink) {
                this._hideActiveLink();
            }

            this.handleDragend(event);
            this._markDirty();
        }
    },

    /**
    Description TODO
    @function
    */
    handleShortcut: {
        enumerable: false,
        value: function(event, action) {
            if (this._actions[action] && this._actions[action].enabled) {
                this.doAction(action);
                return true;
            }

            return false;
        }
    },

    /**
    Description TODO
    @function
    */
    handleMousedown: {
        enumerable: false,
        value: function(event) {
            if (this._resizer) {
                if (this.resizer.startUserAction(event)) {
                    event.preventDefault();
                    event.stopPropagation();

                    return;
                }
            }
        }
    },

    /**
    Description TODO
    @function
    */
    handleMouseup: {
        enumerable: false,
        value: function(event) {
            var thisRef = this,
                element = event.target,
                range,
                offset;

            if (this._resizer) {
                if (this.resizer.endUserAction(event)) {
                    event.preventDefault();
                    event.stopPropagation();

                    return;
                }
            }

            if (element.tagName === "IMG") {
                if (this._currentResizerElement !== element) {
                    this._needsShowResizerOn = element;
                    this.needsDraw = true;
                }
            } else {
                if (this._resizer && this._resizer.element) {
                    this._needsHideResizer = true;
                    this.needsDraw = true;
                }

                if (this._hasSelectionChangeEvent === false) {
                    this.handleSelectionchange();
                }
                this.handleDragend(event);
            }
        }
    },

    /**
    Description TODO
    @function
    */
    handleTouchstart: {
        enumerable: false,
        value: function() {
            this.handleMousedown(event);
        }
    },

    /**
    Description TODO
    @function
    */
    handleTouchend: {
        enumerable: false,
        value: function() {
            this.handleMouseup(event);
        }
    },

    /**
    Description TODO
    @function
    */
    handleSelectionchange: {
        enumerable: false,
        value: function() {
            var thisRef = this,
                range,
                element,
                hideLinkPopup = true;

            if (this._ignoreSelectionchange) {
                return;
            }

            if (this._hasSelectionChangeEvent == null) {
                this._hasSelectionChangeEvent = true;
            }

            if (this._resizer) {
                if (this._selectingResizer !== true && this._resizer.element) {
                    this._needsHideResizer = true;
                    this.needsDraw = true;
                }
            }

            //Check if we are inside an anchor
            range = this._selectedRange;
            if (range && range.collapsed) {
                element = range.commonAncestorContainer;
                while (element && element != this._element) {
                    if (element.nodeName == "A") {
                        hideLinkPopup = false;
                        if (element != this._activeLink) {
                            this._needsActiveLinkOn = element;
                            this.needsDraw = true;
                        }
                        break;
                    }
                    element = element.parentElement;
                }
            }
            if (hideLinkPopup) {
                this._needsActiveLinkOn = null;
                this.needsDraw = true;
            }

            this._statesDirty = true;
            if (this._selectionChangeTimer) {
                clearTimeout(this._selectionChangeTimer);
            }
            this._selectionChangeTimer = setTimeout(function() {
                thisRef._dispatchEditorEvent("editorSelect");
            }, 50);
        }
    },

    /**
    Description TODO
    @function
    */
    handleDragstart: {
        enumerable: false,
        value: function(event) {
            // let's remember which element we are dragging
            this._dragSourceElement = event.srcElement;
        }
    },

    /**
    Description TODO
    @function
    */
    handleDragend: {
        enumerable: false,
        value: function(event) {
            delete this._dragSourceElement;
            delete this._dragOverX;
            delete this._dragOverY;

            this.handleSelectionchange();
        }
    },

    /**
    Description TODO
    @function
    */
    handleDragover: {
        enumerable: false,
        value: function(event) {
            var thisRef = this,
                range;

            // If we are moving an element from within the ourselves, let the browser deal with it...
            if (this._dragSourceElement) {
                return;
            }

            // JFD TODO: check if drop type is acceptable...
            event.dataTransfer.dropEffect = this._allowDrop ? "copy" : "none";

            event.preventDefault();
            event.stopPropagation();

            // Remove the link popup
            if (this._needsActiveLinkOn === false && this._activeLink) {
                this._needsActiveLinkOn = null;
                this.needsDraw = true;
            }

            // Update the caret
            if (event.x !== this._dragOverX || event.y !== this._dragOverY) {
                this._dragOverX = event.x;
                this._dragOverY = event.y;

                this._ignoreSelectionchange = true;
                if (document.caretRangeFromPoint) {
                    range = document.caretRangeFromPoint(event.x, event.y);
                } else if (event.rangeParent && event.rangeOffset) {
                    range = document.createRange();
                    range.setStart(event.rangeParent, event.rangeOffset);
                    range.setEnd(event.rangeParent, event.rangeOffset);
                }

                if (range) {
                    this._selectedRange = range;
                }
                if (this._ignoreSelectionchangeTimer) {
                    clearTimeout(this._ignoreSelectionchangeTimer)
                }
                this._ignoreSelectionchangeTimer = setTimeout(function(){
                    delete thisRef._ignoreSelectionchange;
                    thisRef._ignoreSelectionchangeTimer = null;
                }, 0);
            }
        }
    },

    /**
    Description TODO
    @function
    */
    handleDrop: {
        enumerable: false,
        value: function(event) {
            var thisRef = this,
                files = event.dataTransfer.files,
                fileLength = files.length,
                file,
                data,
                reader,
                i,
                delegateMethod,
                response;

            if (this._dragSourceElement) {
                // Let the browser do the job for us, just make sure we cleanup after us
                this.handleDragend(event);
                this.handleSelectionchange();
                return;
            }

            event.preventDefault();
            event.stopPropagation();

            if (fileLength) {
                for (i = 0; i < fileLength; i ++) {
                    file = files[i];
                    delegateMethod = this._delegateMethod("fileDrop");
                    response = true;

                    if (window.FileReader) {
                        reader = new FileReader();
                        reader.onload = function() {
                            data = reader.result;

                            if (delegateMethod) {
                                response = delegateMethod.call(this.delegate, this, file, data);
                            }
                            if (response === true) {
                                if (file.type.match(/^image\//i)) {
                                    document.execCommand("insertimage", false, data);
                                    thisRef._markDirty();
                                }
                            }
                        }
                        reader.onprogress = function(e) {
                        }
                        reader.onerror = function(e) {
                        }
                        reader.readAsDataURL(file);
                    } else {
                        // Note: This browser does not support the File API, we cannot do a preview...
                        if (delegateMethod) {
                            response = delegateMethod.call(this.delegate, this, file);
                        }
                        if (response === true) {
                            // TODO: for now, we do nothing, up to the consumer to deal with that case
                        }
                    }
                }
            } else {
                data = event.dataTransfer.getData("text/html");
                if (data) {
                    // Sanitize Fragment (CSS & JS)
                    if (this._sanitizer) {
                        data = this._sanitizer.willInsertHTMLData(data, this._uniqueId);
                    }
                } else {
                    data = event.dataTransfer.getData("text/plain") || event.dataTransfer.getData("text");
                    if (data) {
                        var div = document.createElement('div');
                        div.innerText = data;
                        data = div.innerHTML;
                    }
                }
                if (data) {
                    var delegateMethod = this._delegateMethod("drop"),
                        response;

                    if (delegateMethod) {
                        response = delegateMethod.call(this.delegate, this, data, "text/html");
                        if (response === true) {
                            data = data.replace(/\<meta [^>]+>/gi, ""); // Remove the meta tag.
                        } else {
                            data = response === false ? null : response ;
                        }
                    } else {
                        data = data.replace(/\<meta [^>]+>/gi, ""); // Remove the meta tag.
                    }
                    if (data && data.length) {
                        document.execCommand("inserthtml", false, data);
                        this._markDirty();
                    }
                }
            }
            this.handleDragend(event);
        }
    },

    /**
    Description TODO
    @function
    */
    handlePaste: {
        enumerable: false,
        value: function(event) {
            var thisRef = this,
                clipboardData = event.clipboardData,
                data = clipboardData.getData("text/html"),
                delegateMethod,
                response,
                div,
                isHTML,
                item,
                file,
                reader;

            /* NOTE: Chrome, and maybe the other browser too, returns html or plain text data when calling getData("text/html),
                     To determine if the data is actually html, check the data starts with either an html or a meta tag
            */
            isHTML = data && data.match(/^(<meta [^>]*>|<html>)/i);
            if (data && isHTML) {
                // Sanitize Fragment (CSS & JS)
                if (this._sanitizer) {
                    data = this._sanitizer.willInsertHTMLData(data, this._uniqueId);
                }
            } else {
                data = clipboardData.getData("text/plain") ||  clipboardData.getData("text");
                if (data) {
                    // Convert plain text to html
                    div = document.createElement('div');
                    div.innerText = data;
                    data = div.innerHTML;
                }
            }

            if (data) {
                delegateMethod = this._delegateMethod("paste");
                if (delegateMethod) {
                    response = delegateMethod.call(this.delegate, this, data, "text/html");
                    if (response === true) {
                        data = data.replace(/\<meta [^>]+>/gi, ""); // Remove the meta tag.
                    } else {
                        data = response === false ? null : response ;
                    }
                } else {
                    data = data.replace(/\<meta [^>]+>/gi, ""); // Remove the meta tag.
                }
                if (data && data.length) {
                    document.execCommand("inserthtml", false, data);
                    this._markDirty();
                }
            } else {
                // Maybe we have trying to paste an image as Blob...
                if (clipboardData.items.length) {
                    item = clipboardData.items[0];
                    if (item.kind == "file" && item.type.match(/^image\/.*$/)) {
                        file = item.getAsFile();

                        response = true;

                        if (window.FileReader) {
                            reader = new FileReader();
                            reader.onload = function() {
                                data = reader.result;

                                this._delegateMethod("filePaste");
                                if (delegateMethod) {
                                    response = delegateMethod.call(this.delegate, this, file, data);
                                }
                                if (response === true) {
                                    if (file.type.match(/^image\//i)) {
                                        document.execCommand("insertimage", false, data);
                                        thisRef._markDirty();
                                    }
                                }
                            }
                            reader.onprogress = function(e) {
                            }
                            reader.onerror = function(e) {
                            }
                            reader.readAsDataURL(file);
                        } else {
                            // Note: This browser does not support the File API, we cannot handle it directly...
                            if (delegateMethod) {
                                response = delegateMethod.call(this.delegate, this, file);
                            }
                            if (response === true) {
                                // TODO: for now, we do nothing, up to the consumer to deal with that case
                            }
                        }
                    }
                }
            }

            event.preventDefault();
            event.stopPropagation();
        }
    },

    /**
    Description TODO
    @function
    @param {String} pointer TODO
    @param {Component} demandingComponent TODO
    @returns {Boolean} false
    */
    surrenderPointer: {
        value: function(pointer, demandingComponent) {
            return false;
        }
    },

    /**
    Description TODO
    @private
    */
    _observePointer: {
        value: function(pointer) {
            this.eventManager.claimPointer(pointer, this);
            this._observedPointer = pointer;
        }
    },

    /**
    Description TODO
    @private
    */
    _releaseInterest: {
        value: function() {
            this.eventManager.forfeitPointer(this._observedPointer, this);
            this._observedPointer = null;
        }
    },

    // Actions
    /**
    Description TODO
    @function
    */
    handleAction: {
        enumerable: false,
        value: function(event) {
            var target = event.currentTarget,
                action = target.action || target.identifier,
                value = false;
            if (action) {
                if (this._actions[action].needsValue) {
                    value = target.actionValue;
                    if (value !== undefined) {
                        value = target[value];
                        if (value === undefined) {
                            value = target.actionValue;
                        }
                    } else {
                        value = target.value;
                    }

                    if (value === undefined) {
                        value = false;
                    }
                }
                this.doAction(action, value);
            }
        }
    },

    /**
    Description TODO
    @function
    */
    doAction: {
        enumerable: true,
        value: function(action, value) {
            // Check if the action is valid and enabled
            if (this._actions[action] && this._actions[action].enabled === true) {
                if (value === undefined) {
                    value = false;
                }
                document.execCommand(action, false, value);

                this.handleSelectionchange();
                this._markDirty();
            }
        }
    },


    // Private methods
    /**
    Description TODO
    @private
    @function
    */
    _dispatchEditorEvent: {
        enumerable: false,
        value: function(type, value) {
            var editorEvent = document.createEvent("CustomEvent");
            editorEvent.initCustomEvent(type, true, false, value === undefined ? null : value);
            editorEvent.type = type;
            this.dispatchEvent(editorEvent);
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _markDirty: {
        enumerable: false,
        value: function() {
            var thisRef = this,
                updateValues = function() {
                    clearTimeout(thisRef._forceUpdateValuesTimeout);
                    delete thisRef._forceUpdateValuesTimeout;
                    clearTimeout(thisRef._updateValuesTimeout);
                    delete thisRef._updateValuesTimeout;
                    thisRef.dispatchEvent(MutableEvent.changeEventForKeyAndValue("value" , thisRef.value));
                    thisRef.dispatchEvent(MutableEvent.changeEventForKeyAndValue("textValue" , thisRef.textValue));
                    thisRef._dispatchEditorEvent("editorChange");
                };

            if (!this._needsResetContent) {
                // Clear the cached value
                this._dirtyValue = true;
                this._dirtyTextValue = true;
            }

            if (!this._forceUpdateValuesTimeout) {
                this._forceUpdateValuesTimeout = setTimeout(updateValues, 1000);
            }
            if (this._updateValuesTimeout) {
                clearTimeout(this._updateValuesTimeout);
            }
            this._updateValuesTimeout = setTimeout(updateValues, 200);
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _delegateMethod: {
        enumerable: false,
        value: function(name) {
            var delegate, delegateFunctionName, delegateFunction;
            if (typeof this.identifier === "string") {
                delegateFunctionName = this.identifier + name.toCapitalized();
            } else {
                delegateFunctionName = name;
            }
            if ((delegate = this.delegate) && typeof (delegateFunction = delegate[delegateFunctionName]) === "function") {
                return delegateFunction;
            }

            return null;
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _nodeOffset: {
        enumerable: false,
        value: function(node) {
            var parentNode = node.parentNode,
                childNodes = parentNode.childNodes,
                i;

            for (i in childNodes) {
                if (childNodes[i] === node) {
                    return parseInt(i, 10); // i is a string, we need an integer
                }
            }
            return 0;
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _lastInnerNode: {
        enumerable: false,
        value: function() {
            var nodes = this.element.firstChild.childNodes,
                nbrNodes = nodes.length,
                node = null;

            while (nodes) {
                nbrNodes = nodes.length;
                if (nbrNodes) {
                    node = nodes[nbrNodes - 1];
                    nodes = node.childNodes;
                } else {
                    break;
                }
            }

            return node;
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _selectedRange: {
        enumerable: false,
        set: function(range) {
            if (window.getSelection) {
                var selection = window.getSelection();
                selection.removeAllRanges();
                selection.addRange(range);
            } else {
                range.select();
            }
        },

        get: function() {
            var userSelection,
                range;

            if (window.getSelection) {
                userSelection = window.getSelection();
            } else if (document.selection) { // Opera!
                userSelection = document.selection.createRange();
            }

            if (userSelection.getRangeAt) {
                if (userSelection.rangeCount) {
                    return userSelection.getRangeAt(0);
                } else {
                    // return an empty selection
                    return document.createRange();
                }
            }
            else { // Safari!
                var range = document.createRange();
                range.setStart(userSelection.anchorNode, userSelection.anchorOffset);
                range.setEnd(userSelection.focusNode, userSelection.focusOffset);
                return range;
            }
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _equalRange: {
        enumerable: false,
        value: function(rangeA, rangeB) {
            return (rangeA.startContainer == rangeB.startContainer &&
                rangeA.startOffset == rangeB.startOffset &&
                rangeA.endContainer == rangeB.endContainer &&
                rangeA.endOffset == rangeB.endOffset);
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _showActiveLink: {
        enumerable: false,
        value: function(element) {
            var editorElement = this._element.firstChild,
                popup,
                parentNode,
                nextSibling,
                w, h, l, t,
                left, right, leftWidth, rightWidth,
                style,
                popupExtraWidth = 53; // This is depending of the popup css

            var offsetLeft,
                offsetTop,
                _findOffset = function(node) {
                    offsetLeft = node.offsetLeft;
                    offsetTop = node.offsetTop;

                    while ((node = node.offsetParent) && node != editorElement) {
                        offsetLeft += node.offsetLeft;
                        offsetTop += node.offsetTop;
                    }
                };


            if (this._activeLink != element) {
                this._hideActiveLink();
                if (element) {

                    _findOffset(element);

                    parentNode = element.parentNode;
                    nextSibling = element.nextSibling;

                    oh = editorElement.offsetHeight;
                    ow = editorElement.offsetWidth;
                    st = editorElement.scrollTop;
                    sl = editorElement.scrollLeft;

                    w  = element.offsetWidth -1,
                    h  = element.offsetHeight -1,
                    l  = offsetLeft,
                    t  = offsetTop,

                    style = "";

                    // Should we display the popup on top or below the element?
                    if (t > 60 && t - st + h + 50 > oh) {
                        style = "bottom: " + (oh - t + 5) + "px;";
                    } else {
                        style = "top: " + (t + h + 5 ) + "px;";
                    }

                    // Should we display the popup aligned on the left or right of the element?
                    left = sl;
                    right = sl + ow;
                    leftWidth = right - l;
                    rightWidth = l + w - left;

                    if (leftWidth  > rightWidth) {
                        //Let's align the popup to the left of the element or to the far left
                        if (leftWidth < 150) {
                            style += " left: " + (left + 5) + "px;";
                            style += " max-width: " + (ow - 10 - popupExtraWidth) + "px;";
                        } else {
                            style += " left: " + (left + l) + "px;";
                            style += " max-width: " + (leftWidth - 5 - popupExtraWidth) + "px;";
                        }
                    } else {
                        if (rightWidth < 150) {
                            style += " right: " + (left + 6) + "px;";
                            style += " max-width: " + (ow - 10 - popupExtraWidth) + "px;";
                        } else {
                            style += " right: " + (right - (left + l + w + 10)) + "px;";
                            style += " max-width: " + (rightWidth - popupExtraWidth) + "px;";
                        }
                    }

                    popup = document.createElement("DIV");
                    popup.className = "montage-link-popup";
                    popup.setAttribute("contentEditable", "false");
                    popup.setAttribute("style", style);
                    popup.innerHTML = '<a href="' + element.href + '" target="_blank">' + element.href + '</a>';
                    editorElement.insertBefore(popup, null);

                    this._activeLink = element;
                }
            }
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _hideActiveLink: {
        enumerable: false,
        value: function() {
            var popups,
                nbrPopups,
                popup,
                i;

            if (this._activeLink) {
                popups = this._element.firstChild.getElementsByClassName("montage-link-popup");
                nbrPopups = popups.length;

                // Note: We should not have more than one popup, this is just in case...
                for (i = 0; i < nbrPopups; i ++) {
                    popup = popups[0];
                    popup.parentNode.removeChild(popup);
                }

                this._activeLink = null;
            }
        }
    },

    /**
    Description TODO
    @private
    @function
    */
    _cleanupActiveLink: {
        enumerable: false,
        value: function(contentNode) {
            var cleanContentNode = contentNode,
                popups = contentNode.getElementsByClassName("montage-link-popup"),
                nbrPopups,
                popup,
                i;

            if (popups) {
                // We don't want to hide the popup, just return a copy of the content without any popup
                cleanContentNode = contentNode.cloneNode(true);
                popups = cleanContentNode.getElementsByClassName("montage-link-popup");
                nbrPopups = popups.length;

                // Note: We should not have more than one popup, this is just in case...
                for (i = 0; i < nbrPopups; i ++) {
                    popup = popups[0];
                    popup.parentNode.removeChild(popup);
                }
            }

            return cleanContentNode;
        }
    },

    _innerText: {
        enumerable: false,
        value: function(contentNode) {
            var result = "",
                textNodeContents = [],
                newLines = "",
                gotText = false,
                _walkNode = function(node) {
                    var nodeName = node.nodeName,
                        child

                    if (nodeName.match(/^(TITLE|STYLE|SCRIPT)$/)) {
                        return;
                    }

                    if (gotText && nodeName.match(/^(P|DIV|BR|TR|LI)$/)) {
                        newLines += "\n";
                    }

                    for (child = node.firstChild; child; child = child.nextSibling) {
                        if (child.nodeType == 3) {      // text node
                            textNodeContents.push(newLines + child.nodeValue);
                            newLines = "";
                            gotText = true;
                        } else {
                            if (child.nodeName != "BR" || child.nextSibling) {
                                _walkNode(child);
                            }
                        }
                    }

                    if (gotText && nodeName.match(/^(TABLE|UL|OL)$/)) {
                        newLines += "\n";
                    }
                };

            if (contentNode) {
                _walkNode(contentNode);
                result = textNodeContents.join("");
            }

            return result;
        }
    }
});