/* This file contains proprietary software owned by Motorola Mobility, Inc.
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved.
*/ /** @module "montage/ui/rich-text-editor.reel" @requires montage/core/core */ var Montage = require("montage").Montage, Component = require("ui/component").Component, MutableEvent = require("core/event/mutable-event").MutableEvent, Sanitizer = require("./rich-text-sanitizer").Sanitizer, RichTextLinkPopup = require("../overlays/rich-text-linkpopup.reel").RichTextLinkPopup, RichTextResizer = require("../overlays/rich-text-resizer.reel").RichTextResizer, defaultEventManager = require("core/event/event-manager").defaultEventManager, defaultUndoManager = require("core/undo-manager").defaultUndoManager; /** @class module:"montage/ui/rich-text-editor.reel".RichTextEditorBase @extends module:montage/ui/component.Component */ exports.RichTextEditorBase = Montage.create(Component,/** @lends module:"montage/ui/rich-text-editor.reel".RichTextEditor# */ { /** Description TODO @private */ _overlays: { enumerable: false, value: undefined }, /** Description TODO @private */ _overlaySlot: { enumerable: false, value: null }, /** Description TODO @private */ _activeOverlay: { enumerable: false, value: null }, /** Description TODO @private */ _innerElement: { enumerable: false, value: null }, /** Description TODO @private */ _undoManager: { enumerable: false, value: undefined }, /** Description TODO @private */ _isTyping: { enumerable: false, value: false }, /** Description TODO @private */ _startTyping: { enumerable: false, value: function() { if (this._doingUndoRedo) { this._isTyping = false; return; } else if (!this._isTyping) { this._isTyping = true; if (this.undoManager) { this.undoManager.add("Typing", this._undo, this, "Typing", this._innerElement); } } } }, /** Description TODO @private */ _stopTyping: { enumerable: false, value: function() { if (this._isTyping) { this._isTyping = false; } } }, /** 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 */ _contentInitialized: { enumerable: false, value: false }, /** Description TODO @private */ _needsAssignOriginalContent: { enumerable: false, value: true }, /** Description TODO @private */ _needsAssingValue: { enumerable: false, value: false }, /** Description TODO @private */ _setCaretAtEndOfContent: { enumerable: false, value: false }, /** Description TODO @private */ _selectionChangeTimer: { enumerable: false, value: null }, /** Description TODO @private */ _hasFocus: { enumerable: false, value: false }, /** Description TODO @private */ _needsFocus: { value: false }, /** Description TODO @private */ _isActiveElement: { enumerable: false, value: false }, /** Description TODO @private */ _readOnly: { enumerable: false, value: false }, /** Description TODO @private */ _value: { enumerable: false, value: "" }, /** Description TODO @private */ _textValue: { enumerable: false, value: "" }, /** Description TODO @type {} */ delegate: { enumerable: true, value: null }, /** Description TODO @private */ _sanitizer: { enumerable: false, value: undefined }, /** Description TODO @private */ _allowDrop: { enumerable: false, value: true }, // Commands Helpers _getState: { value: function(property, command) { var state; if (this._innerElement == document.activeElement) { state = document.queryCommandValue(command); // Convert string to boolean if (state == "true") { state = true; } if (state == "false") { state = false; } return state; } else { return this["_" + property]; } } }, _genericCommandGetter : { value: function(property, command) { var propertyName = "_" + property; this[propertyName] = this._getState(property, command); return this[propertyName]; } }, _genericCommandSetter : { value: function(property, command, value) { var state = this._getState(property, command); // Make sure the state is up-to-date if (state !== value) { this.doAction(command, typeof value == "boolean" ? false : value); } } }, // Edit Actions & Properties /** Description TODO @private */ _bold: { value: false }, /** Description TODO @private */ _underline: { value: false }, /** Description TODO @private */ _italic: { value: false }, /** Description TODO @private */ _strikeThrough: { value: false }, /** Description TODO @private */ _baselineShiftGetState: { enumerable: false, value: function() { if (this._innerElement == document.activeElement) { if (this._getState("baselineShift", "subscript")) { return "subscript" } else if (this._getState("baselineShift", "superscript")) { return "superscript" } else { return "baseline"; // default } } else { return this._baselineShift; } } }, /** Description TODO @private */ _baselineShift: { value: "baseline" }, /** Description TODO @private */ _listStyleGetState: { enumerable: false, value: function() { if (this._innerElement == document.activeElement) { if (this._getState("listStyle", "insertorderedlist")) { return "ordered" } else if (this._getState("listStyle", "insertunorderedlist")) { return "unordered" } else { return "none"; // default } } else { return this._listStyle; } } }, /** Description TODO @private */ _listStyle: { value: "none" }, /** Description TODO @private */ _justifyGetState: { enumerable: false, value: function() { if (this._innerElement == document.activeElement) { if (this._getState("justify", "justifyleft")) { return "left" } else if (this._getState("justify", "justifycenter")) { return "center" } else if (this._getState("justify", "justifyright")) { return "right" } else if (this._getState("justify", "justifyfull")) { return "full" } else { return "left"; // default } } else { return this._justify; } } }, /** Description TODO @private */ _justify: { value: "left" }, /** Description TODO @private */ _fontNameGetState: { enumerable: false, value: function() { this._fontName = this._getState("fontName", "fontname"); if (this._fontName) { this._fontName = this._fontName.replace(/\"|\'/g, ""); } return this._fontName; } }, /** Description TODO @private */ _fontName: { value: "" }, /** Description TODO @private */ _fontSize: { value: 0 }, /** Description TODO @private */ _backColor: { value: "" }, /** Description TODO @private */ _foreColor: { value: "" }, /** Description TODO @type {Function} */ _updateStates: { enumerable: true, value: function() { var commands = [{property: "bold"}, {property: "underline"}, {property: "italic"}, {property: "strikeThrough"}, {property: "baselineShift", method: this._baselineShiftGetState}, {property: "justify", method: this._justifyGetState}, {property: "listStyle", method: this._listStyleGetState}, {property: "fontName", method: this._fontNameGetState}, {property: "fontSize"}, {property: "backColor"}, {property: "foreColor"} ], nbrCommands = commands.length, command, commandName, propertyName, state, prevState, method, i; if (this._innerElement == document.activeElement) { for (i = 0; i < nbrCommands; i ++) { command = commands[i]; if (typeof command == "object") { propertyName = command.property; commandName = command.name || propertyName.toLowerCase(); method = command.method || this._getState; } else { continue; } if (defaultEventManager.registeredEventListenersForEventType_onTarget_("change@" + propertyName, this)) { prevState = this["_" + propertyName]; state = method.call(this, propertyName, commandName); if (state !== prevState) { this["_" + propertyName] = state; this.dispatchEvent(MutableEvent.changeEventForKeyAndValue(propertyName , prevState).withPlusValue(state)); } } } } } }, // Component Callbacks /** Description TODO @function */ prepareForDraw: { enumerable: false, value: function() { var el = this.element; el.classList.add('montage-editor-container'); el.addEventListener("focus", this); el.addEventListener("dragstart", this, false); el.addEventListener("dragenter", this, false); el.addEventListener("dragover", this, false); el.addEventListener("drop", this, false); el.addEventListener("dragend", this, false); // Setup the sanitizer if not specified if (this._sanitizer === undefined) { this._sanitizer = Sanitizer.create(); } // Setup the undoManager if not specified if (this._undoManager === undefined) { this._undoManager = defaultUndoManager; } // Initialize the overlays if (this._overlays === undefined) { // Install the default overlays this._overlays = [RichTextResizer.create(), RichTextLinkPopup.create()]; } this._callOverlays("initWithEditor", this, true); } }, /** Description TODO @function */ draw: { enumerable: false, value: function() { var editorElement = this.element, editorInnerElement, contents, content, contentChanged, prevValue, i; if (this._needsAssingValue || this._needsAssignOriginalContent) { editorInnerElement = this._innerElement = editorElement.querySelector(".montage-editor"); if (this._contentInitialized) { // if the content has been already initialized, we need replace it by a clone of itself // in order to reset the browser undo stack editorElement.replaceChild(editorInnerElement.cloneNode(true), editorInnerElement); editorInnerElement = this._innerElement = editorElement.querySelector(".montage-editor"); //JFD TODO: Need to clear entries in the Montage undoManager queue } editorInnerElement.setAttribute("contenteditable", (this._readOnly ? "false" : "true")); editorInnerElement.classList.add("editor-" + this._uniqueId); editorInnerElement.innerHTML = ""; if (this._needsAssingValue) { // Set the contentEditable value if (this._value && !this._dirtyValue) { editorInnerElement.innerHTML = this._value; // Since this property affects the textValue, we need to fire a change event for it as well if (defaultEventManager.registeredEventListenersForEventType_onTarget_("change@textValue", this)) { prevValue = this._textValue; if (this.textValue !== prevValue) { this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("textValue" , prevValue).withPlusValue(this.textValue)); } } } else if (this._textValue && !this._dirtyTextValue) { if (editorInnerElement.innerText) { editorInnerElement.innerText = this._textValue; } else { editorInnerElement.textContent = this._textValue; } // Since this property affects the value, we need to fire a change event for it as well if (defaultEventManager.registeredEventListenersForEventType_onTarget_("change@value", this)) { prevValue = this._value; if (this.value !== prevValue) { this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("value" , prevValue).withPlusValue(this.value)); } } } } else if (this._needsAssignOriginalContent) { contents = this.originalContent; contentChanged = false; if (contents instanceof Element) { editorInnerElement.appendChild(contents); contentChanged = true; } else { for (i = 0; (content = contents[i]); i++) { editorInnerElement.appendChild(content); contentChanged = true; } } if (contentChanged) { if (defaultEventManager.registeredEventListenersForEventType_onTarget_("change@value", this)) { prevValue = this._value; if (this.value !== prevValue) { this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("value" , prevValue).withPlusValue(this.value)); } } if (defaultEventManager.registeredEventListenersForEventType_onTarget_("change@textValue", this)) { prevValue = this._textValue; if (this.textValue !== prevValue) { this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("textValue" , prevValue).withPlusValue(this.textValue)); } } // Clear the cached value in order to force an editorChange event this._dirtyValue = true; this._dirtyTextValue = true; } } this._adjustPadding(); this.markDirty(); this._needsAssingValue = false; this._needsAssignOriginalContent = false; this._contentInitialized = true; this._setCaretAtEndOfContent = true; if (this.hasFocus) { // Call focus to move caret to end of document this.focus(); } } else { editorInnerElement = this._innerElement; } if (this._readOnly) { editorInnerElement.setAttribute("contentEditable", "false"); editorElement.classList.add("readonly") } else { editorInnerElement.setAttribute("contentEditable", "true"); editorElement.classList.remove("readonly") } } }, /** Description TODO @function */ didDraw: { value: function() { if (this._needsFocus) { this._innerElement.focus(); if(document.activeElement == this._innerElement) { 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 */ slotDidSwitchContent: { enumerable: false, value: function(substitution, nodeShown, componentShown, nodeHidden, componentHidden) { if(componentHidden && typeof componentHidden.didBecomeInactive === 'function') { componentHidden.didBecomeInactive(); } if(componentShown && typeof componentShown.didBecomeActive === 'function') { componentShown.didBecomeActive(); } } }, /** Description TODO @function */ _adjustPadding: { enumerable: false, value: function() { var el = this._innerElement, 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 = this._innerElement, isActive, savedRange, timer; this._hasFocus = true; this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("hasFocus" , false).withPlusValue(true)); isActive = (content && content === document.activeElement); if (isActive != this._isActiveElement) { this._isActiveElement = isActive; this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("isActiveElement" , false).withPlusValue(true)); } if (this._setCaretAtEndOfContent) { 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._setCaretAtEndOfContent = false; } el.addEventListener("blur", this); el.addEventListener("input", this); el.addEventListener("keydown", this); el.addEventListener("keypress", this); el.addEventListener("cut", this); el.addEventListener("paste", this); 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) as we provide our own document.execCommand("enableObjectResizing", false, false); // Force use css for styling (if supported) document.execCommand("styleWithCSS", false, true); // Update the states now that we have focus this._updateStates(); } }, /** Description TODO @function */ handleBlur: { enumerable: false, value: function() { var el = this.element, content = this._innerElement, isActive; this._hasFocus = false; this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("hasFocus" , true).withPlusValue(false)); isActive = (content && content === document.activeElement); if (isActive != this._isActiveElement) { this._isActiveElement = isActive; this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("isActiveElement" , !isActive).withPlusValue(isActive)); } // Force a selectionchange when we lose the focus this.handleSelectionchange(); el.removeEventListener("blur", this); el.removeEventListener("input", this); el.removeEventListener("keydown", this); el.removeEventListener("keypress", this); el.removeEventListener("cut", this); el.removeEventListener("paste", this); 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); } } }, /** Description TODO @function */ handleKeydown: { enumerable: false, value: function(event) { if (["Left", "Right", "Up", "Down", "Home", "End"].indexOf(event.keyIdentifier) != -1) { this._stopTyping(); } } }, /** Description TODO @function */ handleKeypress: { enumerable: false, value: function() { if (this._hasSelectionChangeEvent === false) { this.handleSelectionchange(); } this.markDirty(); } }, /** Description TODO @function */ handleInput: { enumerable: false, value: function(event) { if (!this._executingCommand && !this._ignoreNextInputEvent) { this._startTyping(); } delete this._ignoreNextInputEvent; if (this._hasSelectionChangeEvent === false) { this.handleSelectionchange(); } this.handleDragend(event); this.markDirty(); } }, /** Description TODO @function */ handleShortcut: { enumerable: false, value: function(event, action) { this.doAction(action); return true; } }, /** Description TODO @function */ handleMousedown: { enumerable: false, value: function(event) { this._savedSelection = this._selectedRange; this._callOverlays(event.type == "mousedown" ? "editorMouseDown" : "editorTouchStart", event); } }, /** Description TODO @function */ handleMouseup: { enumerable: false, value: function(event) { if (!this._equalRange(this._savedSelection, this._selectedRange)) { this._stopTyping(); } if (this._hasSelectionChangeEvent === false) { this.handleSelectionchange(); } this.handleDragend(event); this._callOverlays(event.type == "mouseup" ? "editorMouseUp" : "editorTouchEnd", 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; if (this._hasSelectionChangeEvent == null) { this._hasSelectionChangeEvent = true; } if (this._ignoreSelectionchange || this._equalRange(this._selectedRange, this._savedSelectedRange)) { // no change, ignore return; } this._savedSelectedRange = this._selectedRange; if (this._selectionChangeTimer) { clearTimeout(this._selectionChangeTimer); } this._selectionChangeTimer = setTimeout(function() { thisRef._updateStates(); thisRef._dispatchEditorEvent("editorSelect"); }, 100); this._callOverlays("editorSelectionDidChange", this._savedSelectedRange); } }, /** Description TODO @function */ handleDragstart: { enumerable: false, value: function(event) { var delegateMethod = this._delegateMethod("canDrag"); if (delegateMethod) { if (!delegateMethod.call(this.delegate, this, event.srcElement)) { event.preventDefault(); event.stopPropagation(); return; } } // let's remember which element we are dragging this._dragSourceElement = event.srcElement; } }, /** Description TODO @function */ handleDragenter: { enumerable: false, value: function(event) { this.hideOverlay(); var delegateMethod = this._delegateMethod("canDrop"); if (delegateMethod) { this._allowDrop = delegateMethod.call(this.delegate, this, event, this._dragSourceElement); } else { this._allowDrop = true; } event.dataTransfer.dropEffect = this._allowDrop ? "copy" : "none"; } }, /** 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 && this._allowDrop) { return; } event.dataTransfer.dropEffect = this._allowDrop ? "copy" : "none"; event.preventDefault(); event.stopPropagation(); // Update the caret if (this._allowDrop && (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._stopTyping(); if (this.undoManager) { this.undoManager.add("Move", this._undo, this, "Move", this._innerElement); } this._ignoreNextInputEvent = true; this.handleDragend(event); this.handleSelectionchange(); return; } event.preventDefault(); event.stopPropagation(); if (fileLength) { for (i = 0; i < fileLength; i ++) { file = files[i]; delegateMethod = this._delegateMethod("shouldDropFile"); response = true; if (window.FileReader) { reader = new FileReader(); reader.onload = function() { data = reader.result; if (delegateMethod) { response = delegateMethod.call(thisRef.delegate, thisRef, file, data); } if (response === true) { if (file.type.match(/^image\//i)) { thisRef.execCommand("insertimage", false, data, "Drop"); thisRef.markDirty(); } } else if (typeof response == "string") { thisRef.execCommand("inserthtml", false, response, "Drop"); 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 (typeof response == "string") { thisRef.execCommand("inserthtml", false, response, "Drop"); thisRef.markDirty(); } } } } 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'); if (div.innerText) { div.innerText = data; } else { div.textContent = data; } data = div.innerHTML; } } if (data) { var delegateMethod = this._delegateMethod("shouldDrop"), response; if (delegateMethod) { response = delegateMethod.call(this.delegate, this, event, data, "text/html"); if (response === true) { data = data.replace(/\]+>/gi, ""); // Remove the meta tag. } else { data = response === false ? null : response ; } } else { data = data.replace(/\]+>/gi, ""); // Remove the meta tag. } if (data && data.length) { this.execCommand("inserthtml", false, data, "Drop"); this.markDirty(); } } } this.handleDragend(event); } }, /** Description TODO @function */ handleCut: { enumerable: false, value: function(event) { this._stopTyping() if (this.undoManager) { this.undoManager.add("Cut", this._undo, this, "Cut", this._innerElement); } this._ignoreNextInputEvent = true; } }, /** 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(/^(]*>|)/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'); if (div.innerText) { div.innerText = data; } else { div.textContent = data; } data = div.innerHTML; } } if (data) { delegateMethod = this._delegateMethod("shouldPaste"); if (delegateMethod) { response = delegateMethod.call(this.delegate, this, event, data, "text/html"); if (response === true) { data = data.replace(/\]+>/gi, ""); // Remove the meta tag. } else { data = response === false ? null : response ; } } else { data = data.replace(/\]+>/gi, ""); // Remove the meta tag. } if (data && data.length) { this.execCommand("inserthtml", false, data, "Paste"); 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; thisRef._delegateMethod("shouldPasteFile"); if (delegateMethod) { response = delegateMethod.call(thisRef.delegate, thisRef, file, data); } if (response === true) { if (file.type.match(/^image\//i)) { thisRef.execCommand("insertimage", false, data, "Paste"); 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(); } }, // Actions /** Description TODO @function */ handleAction: { enumerable: false, value: function(event) { var target = event.currentTarget, action = target.action || target.identifier, value = false; if (action) { this.doAction(action, value); } } }, /** Description TODO @function */ doAction: { enumerable: true, value: function(action, value) { this.execCommand(action, false, value); // Force an update states right away this._updateStates(); } }, _undo: { enumerable: false, value: function(label, element) { var editorElement = this._innerElement; if (!element || element === editorElement) { this._doingUndoRedo = true; this._ignoreNextInputEvent = true; document.execCommand("undo", false, null); this.markDirty(); if (this.undoManager) { this.undoManager.add(label, this._redo, this, label, editorElement); } this._doingUndoRedo = false; } } }, /** Description TODO @function */ _redo: { enumerable: false, value: function(label, element) { var editorElement = this._innerElement; if (!element || element === editorElement) { this._doingUndoRedo = true; this._ignoreNextInputEvent = true; document.execCommand("redo", false, null); this.markDirty(); if (this.undoManager) { this.undoManager.add(label, this._undo, this, label, editorElement); } this._doingUndoRedo = false; } } }, // Private methods /** Description TODO @function */ _execCommandLabel : { enumerable: false, value: { bold: "Bold", italic: "Italic", underline: "Underline", strikethrough: "strikeThrough", subscript: "Subscript", superscript: "Superscript", indent: "Indent", outdent: "Outdent", insertorderedlist: "Ordered List", insertunorderedlist: "Unordered List", justifyleft: "Left Align", justifycenter: "Center", justifyright: "Right Align", justifyfull: "Justify", fontname: "Set Font", fontsize: "Set Size", forecolor: "Set Color", backcolor: "Set Color" } }, /** 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 */ _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; } }, _callOverlays: { value: function(method, param, forceCallAll) { var i, activeOverlay = this._activeOverlay, overlay; // Call the active overlay first if (activeOverlay) { if (typeof activeOverlay[method] == "function") { if (activeOverlay[method](param)) { if (!forceCallAll) { return true; } } } } // Then the other overlays for (i in this._overlays) { overlay = this._overlays[i]; if (overlay !== activeOverlay) { if (typeof overlay[method] == "function") { if (overlay[method](param)) { if (!forceCallAll) { return true; } } } } } return false; } }, /** 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 -1; } }, /** Description TODO @private @function */ _lastInnerNode: { enumerable: false, value: function() { var nodes = this._innerElement.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) { if (rangeA === rangeB) { return true; } if (!rangeA || !rangeB) { return false; } return (rangeA.startContainer == rangeB.startContainer && rangeA.startOffset == rangeB.startOffset && rangeA.endContainer == rangeB.endContainer && rangeA.endOffset == rangeB.endOffset); } }, _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; } } });