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 --- .../rich-text-editor.reel/rich-text-editor.css | 112 ++ .../rich-text-editor.reel/rich-text-editor.html | 27 + .../labs/rich-text-editor.reel/rich-text-editor.js | 1668 ++++++++++++++++++++ .../rich-text-editor.reel/rich-text-resizer.js | 349 ++++ .../rich-text-editor.reel/rich-text-sanitizer.js | 132 ++ .../labs/rich-text-editor.reel/shortcut-manager.js | 237 +++ 6 files changed, 2525 insertions(+) create mode 100644 node_modules/labs/rich-text-editor.reel/rich-text-editor.css create mode 100644 node_modules/labs/rich-text-editor.reel/rich-text-editor.html create mode 100644 node_modules/labs/rich-text-editor.reel/rich-text-editor.js create mode 100644 node_modules/labs/rich-text-editor.reel/rich-text-resizer.js create mode 100644 node_modules/labs/rich-text-editor.reel/rich-text-sanitizer.js create mode 100644 node_modules/labs/rich-text-editor.reel/shortcut-manager.js (limited to 'node_modules/labs') diff --git a/node_modules/labs/rich-text-editor.reel/rich-text-editor.css b/node_modules/labs/rich-text-editor.reel/rich-text-editor.css new file mode 100644 index 00000000..656183c4 --- /dev/null +++ b/node_modules/labs/rich-text-editor.reel/rich-text-editor.css @@ -0,0 +1,112 @@ +/* + 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. +
*/ + +.montage-editor { + /* need to be relative in order for the resizer to be positioned correctly */ + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + padding: 4px; + + font-size: 1.0em; + outline: none; + overflow: auto; + z-index: 1; +} + +.montage-editor-frame { + position: relative; + overflow: auto; + height: 100%; + width: 100%; +} + +.montage-resizer-element::selection { + background: rgba(0,0,0,0); +} + + +/* +Resizer +*/ +.montage-resizer { + display: inline-block; +} + +.montage-resizer-frame { + position: absolute; + border: 1px solid black; + z-index: 30; +} + +.montage-resizer-handle { + position: absolute; + border: 1px solid black; + background-color: white; + width: 6px; + height: 6px; + z-index: 31; +} + +.montage-resizer.dragged .montage-resizer-handle{ + display: none; +} + +.montage-resizer-handle:hover { + background-color: black; +} + +.montage-resizer-n { + cursor: n-resize; +} +.montage-resizer-ne { + cursor: ne-resize; +} +.montage-resizer-e { + cursor: e-resize; +} +.montage-resizer-se { + cursor: se-resize; +} +.montage-resizer-s { + cursor: s-resize; +} +.montage-resizer-sw { + cursor: sw-resize; +} +.montage-resizer-w { + cursor: w-resize; +} +.montage-resizer-nw { + cursor: nw-resize; +} + + +/* +Link Popup +*/ +.montage-link-popup { + -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.2); + -webkit-border-radius: 2px; + border-radius: 2px; + position: absolute; + border: 1px solid; + background-color: white; + color: #666; + padding: 12px 20px; + z-index: 50; + cursor: default; + border-color: #BBB #BBB #A8A8A8; + font: 13px/normal "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +.montage-link-popup a { + cursor: pointer; + text-decoration: none; + color: #15C; +} \ No newline at end of file diff --git a/node_modules/labs/rich-text-editor.reel/rich-text-editor.html b/node_modules/labs/rich-text-editor.reel/rich-text-editor.html new file mode 100644 index 00000000..42425b40 --- /dev/null +++ b/node_modules/labs/rich-text-editor.reel/rich-text-editor.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + \ No newline at end of file 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; + } + } +}); diff --git a/node_modules/labs/rich-text-editor.reel/rich-text-resizer.js b/node_modules/labs/rich-text-editor.reel/rich-text-resizer.js new file mode 100644 index 00000000..5da834f4 --- /dev/null +++ b/node_modules/labs/rich-text-editor.reel/rich-text-resizer.js @@ -0,0 +1,349 @@ +/* + 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-resizer.js" + @requires montage/core/core +*/ +var Montage = require("montage/core/core").Montage, + dom = require("montage/ui/dom"), + Point = require("montage/core/geometry/point").Point; + +/** + @class module:"montage/ui/rich-text-resizer.js".Resizer + @extends module:montage/core/core.Montage +*/ +exports.Resizer = Montage.create(Montage,/** @lends module:"montage/ui/rich-text-resizer.js".Resizer# */ { + + _editor: { + value: null + }, + + _element: { + value: null + }, + + element: { + get: function() { + return this._element; + } + }, + + initialize: { + value: function(editor) { + this._editor = editor; + } + }, + + show: { + value: function(element) { + // Remove the current resizer + if (this._element) { + this._removeResizer(element); + } + if (element) { + this._addResizer(element); + } + this._element = element; + } + }, + + hide: { + value: function() { + this._removeResizer(this._element); + this._element = null; + } + }, + + cleanup: { + value: function(contentNode) { + var cleanContentNode = contentNode, + resizers = contentNode.getElementsByClassName("montage-resizer"), + nbrResizers, + resizer, + i; + + if (resizers) { + // We don't want to hide the resizer, just return a copy of the content without the resizer + cleanContentNode = contentNode.cloneNode(true); + resizers = cleanContentNode.getElementsByClassName("montage-resizer"); + nbrResizers = resizers.length; + + // Note: We should not have more than one resizer, this is just in case... + for (i = 0; i < nbrResizers; i ++) { + resizer = resizers[0]; + resizer.parentNode.removeChild(resizer); + } + } + + return cleanContentNode; + } + }, + + draw : { + value: function() { + var thisRef = this; + + if (this._draggedElement) { + // Resize the resizer frame + var frame = this._draggedElement.parentNode.firstChild, + zero = Point.create().init(0, 0), + framePosition = dom.convertPointFromNodeToPage(frame, zero), + cursor = this._cursorPosition, + direction = this._draggedElement.id.substring("editor-resizer-".length), + info = this._resizerFrameInfo, + ratio = info.ratio, + height = frame.clientHeight, + width = frame.clientWidth, + top = parseFloat(frame.style.top, 10), + left = parseFloat(frame.style.left, 10), + minSize = 15; + + element = this._draggedElement.parentNode.previousSibling; + + if (direction == "n") { + height += framePosition.y - cursor.y; + top = info.top - (height - info.height); + } else if (direction == "ne") { + height += framePosition.y - cursor.y; + width = Math.round(height * ratio); + if (cursor.x > (framePosition.x + width)) { + width = cursor.x - framePosition.x; + height = Math.round(width / ratio); + } + top = info.top - (height - info.height); + } else if (direction == "e") { + width = cursor.x - framePosition.x; + } else if (direction == "se") { + height = cursor.y - framePosition.y; + width = Math.round(height * ratio); + if (cursor.x > (framePosition.x + width)) { + width = cursor.x - framePosition.x; + height = Math.round(width / ratio); + } + } else if (direction == "s") { + height = cursor.y - framePosition.y; + } else if (direction == "sw") { + height = cursor.y - framePosition.y; + width = Math.round(height * ratio); + if (cursor.x <= framePosition.x - width + frame.clientWidth) { + width = frame.clientWidth + framePosition.x - cursor.x; + height = Math.round(width / ratio); + } + left = info.left - (width - info.width); + } else if (direction == "w") { + width += framePosition.x - cursor.x; + left = info.left - (width - info.width); + } else if (direction == "nw") { + height += framePosition.y - cursor.y; + width = Math.round(height * ratio); + if (cursor.x <= framePosition.x - width + frame.clientWidth) { + width = frame.clientWidth + framePosition.x - cursor.x; + height = Math.round(width / ratio); + } + top = info.top - (height - info.height); + left = info.left - (width - info.width); + } + + //set the frame's new height and width + if (height > minSize && width > minSize) { + frame.style.height = height + "px"; + frame.style.width = width + "px"; + frame.style.top = top + "px"; + frame.style.left = left + "px"; + } + + if (this._finalizeDrag) { + this._draggedElement.parentNode.classList.remove("dragged"); + delete this._finalizeDrag; + delete this._resizerFrameInfo; + delete this._draggedElement; + + // Remove the resizer, we don't wont it in case of undo! + this._removeResizer(element); + + // Prevent the editor to try to delete the resizer from now on due to a selection change + this._editor._selectingResizer = true; + + // Take the element offline to modify it + var div = document.createElement("div"), + offlineElement, + savedID; + div.innerHTML = element ? element.outerHTML : ""; + offlineElement = div.firstChild; + + // Resize the element now that it's offline + offlineElement.width = (width + 1); + offlineElement.height = (height +