From 2e04af953463643791f6362bd8ef4c6ba190abfa Mon Sep 17 00:00:00 2001 From: Valerio Virgillito Date: Wed, 18 Apr 2012 13:48:51 -0700 Subject: Squashed commit of the following: commit 2054551bfb01a0f4ca2e138b9d724835462d45cd Merge: 765c2da 616a853 Author: Valerio Virgillito Date: Wed Apr 18 13:48:21 2012 -0700 Merge branch 'refs/heads/master' into integration commit 765c2da8e1aa03550caf42b2bd5f367555ad2843 Author: Valerio Virgillito Date: Tue Apr 17 15:29:41 2012 -0700 updating the picasa carousel Signed-off-by: Valerio Virgillito commit 9484f1c82b81e27edf2dc0a1bcc1fa3b12077406 Merge: d27f2df cacb4a2 Author: Valerio Virgillito Date: Tue Apr 17 15:03:50 2012 -0700 Merge branch 'refs/heads/master' into integration commit d27f2df4d846064444263d7832d213535962abe7 Author: Valerio Virgillito Date: Wed Apr 11 10:39:36 2012 -0700 integrating new picasa carousel component Signed-off-by: Valerio Virgillito commit 6f98384c9ecbc8abe55ccfe1fc25a0c7ce22c493 Author: Valerio Virgillito Date: Tue Apr 10 14:33:00 2012 -0700 fixed the text area case issue Text area was renamed from TextArea to Textarea Signed-off-by: Valerio Virgillito commit 1e83e26652266136802bc7af930379c1ecd631a6 Author: Valerio Virgillito Date: Mon Apr 9 22:10:45 2012 -0700 integrating montage v0.8 into ninja. Signed-off-by: Valerio Virgillito Signed-off-by: Valerio Virgillito --- .../rich-text-editor.reel/rich-text-editor-base.js | 1706 ++++++++++++++++++++ 1 file changed, 1706 insertions(+) create mode 100644 node_modules/montage/ui/rich-text-editor/rich-text-editor.reel/rich-text-editor-base.js (limited to 'node_modules/montage/ui/rich-text-editor/rich-text-editor.reel/rich-text-editor-base.js') diff --git a/node_modules/montage/ui/rich-text-editor/rich-text-editor.reel/rich-text-editor-base.js b/node_modules/montage/ui/rich-text-editor/rich-text-editor.reel/rich-text-editor-base.js new file mode 100644 index 00000000..e92da424 --- /dev/null +++ b/node_modules/montage/ui/rich-text-editor/rich-text-editor.reel/rich-text-editor-base.js @@ -0,0 +1,1706 @@ +/* + 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(text.value)); + } + } + } 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(text.value)); + } + } + + // 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._nextInputIsNotTyping) { + this._startTyping(); + } + delete this._nextInputIsNotTyping; + + 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._nextInputIsNotTyping = 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._nextInputIsNotTyping = 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) { + var savedActiveElement = document.activeElement, + editorElement = this._innerElement; + + if (!editorElement) { + return; + } + + // Make sure we are the active element before calling execCommand + if (editorElement != savedActiveElement) { + editorElement.focus(); + } + + if (value === undefined) { + value = false; + } + + this.execCommand(action, false, value); + + // Force an update states right away + this._updateStates(); + + this.handleSelectionchange(); + this.markDirty(); + + // Reset the focus + if (editorElement != savedActiveElement) { + savedActiveElement.focus(); + } + } + }, + + + _undo: { + enumerable: false, + value: function(label, element) { + var editorElement = this._innerElement; + if (!element || element === editorElement) { + this._doingUndoRedo = 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; + 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; + } + } +}); -- cgit v1.2.3