From 7f8730c3add146f1ba107e6fc22d1f5a8348ed8b Mon Sep 17 00:00:00 2001 From: Armen Kesablyan Date: Tue, 7 Feb 2012 16:43:22 -0800 Subject: Refactored rich text editor location --- .../labs/rich-text-editor.reel/rich-text-editor.js | 1668 ++++++++++++++++++++ 1 file changed, 1668 insertions(+) create mode 100644 node_modules/labs/rich-text-editor.reel/rich-text-editor.js (limited to 'node_modules/labs/rich-text-editor.reel/rich-text-editor.js') diff --git a/node_modules/labs/rich-text-editor.reel/rich-text-editor.js b/node_modules/labs/rich-text-editor.reel/rich-text-editor.js new file mode 100644 index 00000000..3fece294 --- /dev/null +++ b/node_modules/labs/rich-text-editor.reel/rich-text-editor.js @@ -0,0 +1,1668 @@ +/* + 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/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; + +/** + @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 == "
") { + // when the contentEditable div is emptied, Chrome add a
, 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() { + this.updateStates(); + return this._states; + } + }, + + /** + Description TODO + @type {Function} + */ + updateStates: { + enumerable: true, + value: function() { + var actions = this._actions, + key, + action, + states, + state, + statesChanged = false, + 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; + statesChanged = true; + } + } + + if (statesChanged) { + this._states = states; + // As we do not use a setter, we need to manually dispatch a change event + this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("states" , this._states)); + } + + } + } + + 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}, + 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 = '
'; + + // 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(); + 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(); + + // 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(/\]+>/gi, ""); // Remove the meta tag. + } else { + data = response === false ? null : response ; + } + } else { + data = data.replace(/\]+>/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(/^(]*>|)/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(/\]+>/gi, ""); // Remove the meta tag. + } else { + data = response === false ? null : response ; + } + } else { + data = data.replace(/\]+>/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"); + }; + + // 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, docH, docW, + maxWidth, + style, + popupExtraWidth = 53; // This is depending of the popup css + + if (this._activeLink != element) { + this._hideActiveLink(); + if (element) { + parentNode = element.parentNode; + nextSibling = element.nextSibling; + + // sanity check: make sure we don't already have a popup installed for that element + if (!nextSibling || nextSibling.tagName !== "DIV" || !nextSibling.classList.contains("montage-link-popup")) { + + oh = editorElement.offsetHeight; + ow = editorElement.offsetWidth; + st = editorElement.scrollTop; + sl = editorElement.scrollLeft; + + w = element.offsetWidth -1, + h = element.offsetHeight -1, + l = element.offsetLeft, + t = element.offsetTop, + + style = ""; + if (t > 60 && t - st + h + 50 > oh) { + style = "bottom: " + (oh - t + 5) + "px;"; + } else { + style = "top: " + (t + h + 5 ) + "px;"; + } + + var maxWidth = ow - l - popupExtraWidth + sl; + if (maxWidth < 150) { + maxWidth = 150; + } + if (l + maxWidth + popupExtraWidth - sl > ow) { + l = ow - maxWidth - popupExtraWidth + sl; + } + if (l < 3) { + l = 3; + } + style += " left: " + l + "px;" + style += "max-width: " + maxWidth + "px;" + + + popup = document.createElement("DIV"); + popup.className = "montage-link-popup"; + popup.setAttribute("contentEditable", "false"); + popup.setAttribute("style", style); + popup.innerHTML = '' + element.href + ''; + parentNode.insertBefore(popup, nextSibling); + + 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 = [], + _walkNode = function(node) { + var child; + + if (node.nodeName == "STYLE") { + return; + } + + // TODO: We need to insert newlines after block elements (and remove any newline coming from HTML) + for (child = node.firstChild; child; child = child.nextSibling) { + if (child.nodeType == 3) { // text node + textNodeContents.push(child.nodeValue); + } else { + _walkNode(child); + } + } + }; + + _walkNode(contentNode); + result = textNodeContents.join(""); + + return result; + } + } +}); -- cgit v1.2.3 From ce963d45cdf2703d2d4eedfa94394b178f86e0f1 Mon Sep 17 00:00:00 2001 From: Armen Kesablyan Date: Thu, 9 Feb 2012 17:11:38 -0800 Subject: More Text Tool Updates --- .../labs/rich-text-editor.reel/rich-text-editor.js | 166 +++++++++++++-------- 1 file changed, 107 insertions(+), 59 deletions(-) (limited to 'node_modules/labs/rich-text-editor.reel/rich-text-editor.js') diff --git a/node_modules/labs/rich-text-editor.reel/rich-text-editor.js b/node_modules/labs/rich-text-editor.reel/rich-text-editor.js index 3fece294..b88d5868 100644 --- a/node_modules/labs/rich-text-editor.reel/rich-text-editor.js +++ b/node_modules/labs/rich-text-editor.reel/rich-text-editor.js @@ -12,6 +12,8 @@ var Montage = require("montage/core/core").Montage, 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 @@ -148,7 +150,6 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ if (this._resizer) { this._needsHideResizer = true; } - if (this._sanitizer) { value = this._sanitizer.willSetValue(value, this._uniqueId); } @@ -296,7 +297,9 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ states: { enumerable: true, get: function() { - this.updateStates(); + if (this._statesDirty || !this._states) { + this.updateStates(); + } return this._states; } }, @@ -313,7 +316,6 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ action, states, state, - statesChanged = false, hasFocus = this._hasFocus; if (this._states == null || this._statesDirty) { @@ -339,16 +341,8 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ if (states[key] !== state) { states[key] = state; - statesChanged = true; } } - - if (statesChanged) { - this._states = states; - // As we do not use a setter, we need to manually dispatch a change event - this.dispatchEvent(MutableEvent.changeEventForKeyAndValue("states" , this._states)); - } - } } @@ -388,6 +382,10 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ 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}, @@ -536,6 +534,7 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ } this._adjustPadding(); + this._markDirty(); delete this._needsResetContent; } @@ -1011,6 +1010,12 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ 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; @@ -1365,9 +1370,11 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ thisRef._dispatchEditorEvent("editorChange"); }; - // Clear the cached value - this._dirtyValue = true; - this._dirtyTextValue = true; + if (!this._needsResetContent) { + // Clear the cached value + this._dirtyValue = true; + this._dirtyTextValue = true; + } if (!this._forceUpdateValuesTimeout) { this._forceUpdateValuesTimeout = setTimeout(updateValues, 1000); @@ -1519,60 +1526,85 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ popup, parentNode, nextSibling, - w, h, l, t, docH, docW, - maxWidth, + 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; - // sanity check: make sure we don't already have a popup installed for that element - if (!nextSibling || nextSibling.tagName !== "DIV" || !nextSibling.classList.contains("montage-link-popup")) { + oh = editorElement.offsetHeight; + ow = editorElement.offsetWidth; + st = editorElement.scrollTop; + sl = editorElement.scrollLeft; - oh = editorElement.offsetHeight; - ow = editorElement.offsetWidth; - st = editorElement.scrollTop; - sl = editorElement.scrollLeft; + w = element.offsetWidth -1, + h = element.offsetHeight -1, + l = offsetLeft, + t = offsetTop, - w = element.offsetWidth -1, - h = element.offsetHeight -1, - l = element.offsetLeft, - t = element.offsetTop, + style = ""; - style = ""; - 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 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;"; + } - var maxWidth = ow - l - popupExtraWidth + sl; - if (maxWidth < 150) { - maxWidth = 150; - } - if (l + maxWidth + popupExtraWidth - sl > ow) { - l = ow - maxWidth - popupExtraWidth + sl; + // 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;"; } - if (l < 3) { - l = 3; + } 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;"; } - style += " left: " + l + "px;" - style += "max-width: " + maxWidth + "px;" - + } - popup = document.createElement("DIV"); - popup.className = "montage-link-popup"; - popup.setAttribute("contentEditable", "false"); - popup.setAttribute("style", style); - popup.innerHTML = '' + element.href + ''; - parentNode.insertBefore(popup, nextSibling); + popup = document.createElement("DIV"); + popup.className = "montage-link-popup"; + popup.setAttribute("contentEditable", "false"); + popup.setAttribute("style", style); + popup.innerHTML = '' + element.href + ''; + editorElement.insertBefore(popup, null); - this._activeLink = element; - } + this._activeLink = element; } } } @@ -1642,25 +1674,41 @@ exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/ value: function(contentNode) { var result = "", textNodeContents = [], + newLines = "", + gotText = false, _walkNode = function(node) { - var child; + var nodeName = node.nodeName, + child - if (node.nodeName == "STYLE") { + if (nodeName.match(/^(TITLE|STYLE|SCRIPT)$/)) { return; } - // TODO: We need to insert newlines after block elements (and remove any newline coming from HTML) + if (gotText && nodeName.match(/^(P|DIV|BR|TR|LI)$/)) { + newLines += "\n"; + } + for (child = node.firstChild