From 2082fa6912eec2ffabd2081b7706e8e1b88a9711 Mon Sep 17 00:00:00 2001 From: Armen Kesablyan Date: Thu, 2 Feb 2012 19:03:59 -0800 Subject: Initial Text Tool Implementation Has rich-text-editor in place need to place with finalized version when complete --- _scss/compass_app_log.txt | 7 + _scss/imports/scss/_Stage.scss | 14 + css/ninja.css | 4 + js/panels/CSSPanel/CSSPanel.js | 6 +- js/stage/stage.reel/stage.html | 12 +- js/stage/stage.reel/stage.js | 7 + js/tools/TextTool.js | 96 +- .../ui/rich-text-editor.reel/rich-text-editor.css | 112 ++ .../ui/rich-text-editor.reel/rich-text-editor.html | 27 + .../ui/rich-text-editor.reel/rich-text-editor.js | 1669 ++++++++++++++++++++ .../ui/rich-text-editor.reel/rich-text-resizer.js | 349 ++++ .../rich-text-editor.reel/rich-text-sanitizer.js | 132 ++ 12 files changed, 2425 insertions(+), 10 deletions(-) create mode 100644 node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.css create mode 100644 node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.html create mode 100644 node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.js create mode 100644 node_modules/montage/ui/rich-text-editor.reel/rich-text-resizer.js create mode 100644 node_modules/montage/ui/rich-text-editor.reel/rich-text-sanitizer.js diff --git a/_scss/compass_app_log.txt b/_scss/compass_app_log.txt index 6fe64a6f..d28e1e77 100644 --- a/_scss/compass_app_log.txt +++ b/_scss/compass_app_log.txt @@ -93,3 +93,10 @@ 2012-01-20 15:16:38 overwrite ../css/ninja.css 2012-01-20 15:17:03 overwrite ../css/ninja.css 2012-01-20 15:17:39 overwrite ../css/ninja.css +2012-01-30 16:28:54 overwrite ../css/ninja.css +2012-01-30 16:29:04 overwrite ../css/ninja.css +2012-01-30 16:33:31 overwrite ../css/ninja.css +2012-01-30 17:18:02 overwrite ../css/ninja.css +2012-01-30 18:28:44 overwrite ../css/ninja.css +2012-02-02 17:20:26 overwrite ../css/ninja.css +2012-02-02 17:20:31 identical ../css/ninja.css diff --git a/_scss/imports/scss/_Stage.scss b/_scss/imports/scss/_Stage.scss index a992b793..09dae3d6 100644 --- a/_scss/imports/scss/_Stage.scss +++ b/_scss/imports/scss/_Stage.scss @@ -192,3 +192,17 @@ overflow-x: auto; overflow-y: auto; } +.montage-editor-frame { + position:absolute; + z-index:7; + top: 0; + left:0; + display:none; + -webkit-user-select: initial; +} +.montage-editor { + padding:0px; + word-wrap: normal; + + + } \ No newline at end of file diff --git a/css/ninja.css b/css/ninja.css index 2fcb1380..7bc4c80e 100644 --- a/css/ninja.css +++ b/css/ninja.css @@ -271,6 +271,10 @@ background: transparent; #mainContent .CodeMirror-scroll { height: 100%; overflow: scroll; overflow-x: auto; overflow-y: auto; } +.montage-editor-frame { position: absolute; z-index: 7; top: 0; left: 0; display: none; -webkit-user-select: initial; } + +.montage-editor { padding: 0px; word-wrap: normal; } + /* PanelUI.scss Styles governing the panels in the UI. Note that colors and font definitions go in _scss/themes/themename/_colors.scss and _scss/themes/themename/_fonts.scss */ /* layout for the container of all panels within a dock area */ .panelContainer { margin: 0px; padding: 0px 0px; position: relative; /* diff --git a/js/panels/CSSPanel/CSSPanel.js b/js/panels/CSSPanel/CSSPanel.js index 94860b30..cf8880a3 100644 --- a/js/panels/CSSPanel/CSSPanel.js +++ b/js/panels/CSSPanel/CSSPanel.js @@ -20,9 +20,9 @@ exports.CSSPanel = Montage.create(PanelBase, { init : { enumerable:true, value : function (){ - this.minHeight = 300; - this.contentHeight = 300; - this.defaultHeight= 300; + this.minHeight = 195; + this.contentHeight = 195; + this.defaultHeight= 195; /* OLD WAY -- Removing the temporary div // TODO: Remove this comment once this is tested. diff --git a/js/stage/stage.reel/stage.html b/js/stage/stage.reel/stage.html index 49d10baf..07b823a7 100644 --- a/js/stage/stage.reel/stage.html +++ b/js/stage/stage.reel/stage.html @@ -34,6 +34,14 @@ } } }, + + "textTool": { + "module": "montage/ui/rich-text-editor.reel", + "name": "RichTextEditor", + "properties": { + "element" : {"#": "textToolObject"} + } + }, "owner": { "module": "js/stage/stage.reel", @@ -46,7 +54,8 @@ "_canvas": {"#": "stageCanvas"}, "_drawingCanvas": {"#": "drawingCanvas"}, "stageDeps": {"@": "StageDeps1"}, - "layout": {"@": "layout1"} + "layout": {"@": "layout1"}, + "textTool": {"@": "textTool"} }, "bindings": { "currentDocumentStageView": { @@ -64,6 +73,7 @@
+
asdasd asd asd asd asd asd
diff --git a/js/stage/stage.reel/stage.js b/js/stage/stage.reel/stage.js index 3e0b852e..96bfccdd 100644 --- a/js/stage/stage.reel/stage.js +++ b/js/stage/stage.reel/stage.js @@ -12,6 +12,7 @@ var vecUtils = require("js/helper-classes/3D/vec-utils").VecUtils; exports.Stage = Montage.create(Component, { + textTool: { value: null }, // TODO - Need to figure out how to remove this dependency // Needed by some tools that depend on selectionDrawn event to set up some logic drawNow: { value : false }, @@ -780,6 +781,12 @@ exports.Stage = Montage.create(Component, { } }, + toViewportCoordinates: { + value: function(x,y) { + return [x + this._userContentLeft, y + this._userContentTop]; + } + }, + setZoom: { value: function(value) { if(!this._firstDraw) diff --git a/js/tools/TextTool.js b/js/tools/TextTool.js index 538583ee..8b48ff4f 100644 --- a/js/tools/TextTool.js +++ b/js/tools/TextTool.js @@ -6,12 +6,30 @@ No rights, expressed or implied, whatsoever to this software are provided by Mot var Montage = require("montage/core/core").Montage, DrawingTool = require("js/tools/drawing-tool").DrawingTool; + RichTextEditor = require("montage/ui/rich-text-editor.reel").RichTextEditor; exports.TextTool = Montage.create(DrawingTool, { + + _selectedElement: { value : null }, + + selectedElement: { + get: function() { + return this._selectedElement; + }, + set: function(val) { + if(this._selectedElement !== null) { + + } + this._selectedElement = val; + } + }, + + drawingFeedback: { value: { mode: "Draw3D", type: "rectangle" } }, HandleLeftButtonDown: { value: function(event) { + this.deselectText(); this.startDraw(event); } }, @@ -50,24 +68,88 @@ exports.TextTool = Montage.create(DrawingTool, { if(drawData) { //this.insertElement(drawData); } - + this._hasDraw = false; this.endDraw(event); } else { - this.doSelection(event); + console.log("im here"); + if (this.application.ninja.selectedElements.length !== 0 ) { + this.selectedElement = this.application.ninja.selectedElements[0]._element; + this.drawTextTool(); + } this._isDrawing = false; } } }, + applyElementStyles : { + value: function(fromElement, toElement, styles) { + styles.forEach(function(style) { + var styleCamelCase = style.replace(/(\-[a-z])/g, function($1){return $1.toUpperCase().replace('-','');}); + console.log(styleCamelCase, style, window.getComputedStyle(fromElement)[style]); + toElement.style[styleCamelCase] = window.getComputedStyle(fromElement)[style]; + }, this); + } + }, + + drawTextTool: { + value: function() { + console.log(" now im here"); + this.application.ninja.stage.textTool.value = this.selectedElement.innerHTML; + if(this.application.ninja.stage.textTool.value === "") { this.application.ninja.stage.textTool.value = " "; } + this.selectedElement.innerHTML = ""; + + //Styling Options for text tool to look identical to the text you are manipulating. + this.application.ninja.stage.textTool.element.style.display = "block"; + this.application.ninja.stage.textTool.element.style.position = "absolute"; + + // Set Top & Left Positions + var textToolCoordinates = this.application.ninja.stage.toViewportCoordinates(this.selectedElement.offsetLeft, this.selectedElement.offsetTop); + this.application.ninja.stage.textTool.element.style.left = textToolCoordinates[0] + "px"; + this.application.ninja.stage.textTool.element.style.top = textToolCoordinates[1] + "px"; + + // Set Width, Height + this.application.ninja.stage.textTool.element.style.width = this.selectedElement.offsetWidth + "px"; + this.application.ninja.stage.textTool.element.style.height = this.selectedElement.offsetHeight + "px"; + + + // Set font styling (Size, Style, Weight) + + me = this; + this.application.ninja.stage.textTool.didDraw = function() { + me.applyElementStyles(me.selectedElement, me.application.ninja.stage.textTool.element, ["overflow"]); + me.applyElementStyles(me.selectedElement, me.application.ninja.stage.textTool.element.firstChild, ["font","padding-left","padding-top","padding-right","padding-bottom", "color"]); + var range = document.createRange(), + sel = window.getSelection(); + sel.removeAllRanges(); + range.selectNodeContents(this.application.ninja.stage.textTool.element.firstChild); + sel.addRange(range); + this.didDraw = function() {}; + console.log("im drew here"); + } + console.log("i end here"); + } + }, + + + deselectText: { + value: function() { + this.application.ninja.stage.textTool.element.style.display = "none"; + this.selectedElement.innerHTML = this.application.ninja.stage.textTool.value; + this.application.ninja.stage.textTool.value = ""; + } + }, + HandleDoubleClick: { value: function(e) { - console.log(this.application.ninja.selectedElements[0]._element); - this.application.ninja.selectedElements[0]._element.setAttribute("contenteditable", true); - this.application.ninja.stage._iframeContainer.style.zIndex = 200; - this.application.ninja.selectedElements[0]._element.focus(); + //this.application.ninja.selectedElements[0]._element.setAttribute("contenteditable", true); + + //if (!this.application.ninja.textTool) { + + //} + } @@ -75,10 +157,12 @@ exports.TextTool = Montage.create(DrawingTool, { Configure: { value: function(wasSelected) { + if(wasSelected) { NJevent("enableStageMove"); this.application.ninja.stage.stageDeps.snapManager.setupDragPlaneFromPlane( workingPlane ); } else { + this.deselectText(); NJevent("disableStageMove"); } } diff --git a/node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.css b/node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.css new file mode 100644 index 00000000..656183c4 --- /dev/null +++ b/node_modules/montage/ui/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/montage/ui/rich-text-editor.reel/rich-text-editor.html b/node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.html new file mode 100644 index 00000000..0856043f --- /dev/null +++ b/node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.js b/node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.js new file mode 100644 index 00000000..37fb9599 --- /dev/null +++ b/node_modules/montage/ui/rich-text-editor.reel/rich-text-editor.js @@ -0,0 +1,1669 @@ +/* + 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, + Resizer = require("./rich-text-resizer").Resizer, + Sanitizer = require("./rich-text-sanitizer").Sanitizer, + Point = require("core/geometry/point").Point; + +/** + @class module:"montage/ui/rich-text-editor.reel".RichTextEditor + @extends module:montage/ui/component.Component +*/ +exports.RichTextEditor = Montage.create(Component,/** @lends module:"montage/ui/rich-text-editor.reel".RichTextEditor# */ { + + /** + Description TODO + @private + */ + _hasSelectionChangeEvent: { + enumerable: false, + value: null // Need to be preset to null, will be set to true or false later on + }, + + /** + Description TODO + @private + */ + _uniqueId: { + enumerable: false, + value: Math.floor(Math.random() * 1000) + "-" + Math.floor(Math.random() * 1000) + }, + + /** + Description TODO + @private + */ + _needsSelectionReset: { + enumerable: false, + value: false + }, + + /** + Description TODO + @private + */ + _selectionChangeTimer: { + enumerable: false, + value: null + }, + + /** + Description TODO + @private + */ + _activeLink: { + enumerable: false, + value: null + }, + + /** + Description TODO + @private + */ + _needsActiveLinkOn: { + enumerable: false, + value: false + }, + + /** + Description TODO + @private + */ + _hasFocus: { + enumerable: false, + value: false + }, + + /** + Description TODO + @type {Function} + */ + hasFocus: { + enumerable: true, + get: function() { + return this._hasFocus; + } + }, + + /** + Description TODO + @private + */ + _dirty: { + enumerable: false, + value: false + }, + + /** + Description TODO + @private + */ + _value: { + enumerable: false, + value: "" + }, + + /** + Description TODO + @type {Function} + */ + value: { + enumerable: true, + serializable: true, + get: function() { + var contentNode = this.element.firstChild, + content; + + if (this._dirtyValue) { + if (this._resizer) { + contentNode = this._resizer.cleanup(contentNode); + } + + contentNode = this._cleanupActiveLink(contentNode); + + content = contentNode ? contentNode.innerHTML : ""; + if (content == "
") { + // 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; + } +