From d59a51618ce22a44697a78b9d23dee9818a78d19 Mon Sep 17 00:00:00 2001 From: Joe Shelby Date: Fri, 7 Jan 2011 11:33:30 -0500 Subject: [PATCH] content editable refactoring, major changes around selection and detecting (new) hyperlink, bug fixes around removing hyperlink --- .../foundation/views/content_editable.js | 1395 +++++++++-------- 1 file changed, 755 insertions(+), 640 deletions(-) diff --git a/frameworks/foundation/views/content_editable.js b/frameworks/foundation/views/content_editable.js index 7845ac8..0b81ff8 100644 --- a/frameworks/foundation/views/content_editable.js +++ b/frameworks/foundation/views/content_editable.js @@ -1,7 +1,7 @@ // ========================================================================== // SCUI.ContentEditableView // ========================================================================== -/*globals NodeFilter*/ +/*globals NodeFilter SC SCUI sc_require */ sc_require('core'); sc_require('panes/context_menu_pane'); @@ -16,15 +16,23 @@ sc_require('panes/context_menu_pane'); @extends SC.WebView @author Mohammed Taher - @version 0.914 - + @author Joe Shelby + @version 0.930 + + ========== + = v.930 = + ========== + - siginificant fixes to selection, architecture, bug fixes + - selected image src now available as calculated property + - ctrl-a / cmd-a triggers querySelection + ========== = v.920 = ========== - Added new functionality related to images. Users can bind to the currently selected image's width, height or alt/label property. I also added a function to reset the dimensions of the (selected) image. - + ========== = v.914 = ========== @@ -32,13 +40,13 @@ sc_require('panes/context_menu_pane'); according to the value on the tabSize option - Commented out querying indent/outdent as it's a buggy implemention. Querying them now will always return NO - + ========== = v.9131 = ========== - No longer explicity setting the scrolling attribute if allowScrolling is YES (scroll bars were being rendered at all times) - COMMIT HAS BEEN REVERTED - + ========== = v0.913 = ========== @@ -62,25 +70,26 @@ sc_require('panes/context_menu_pane'); c. _getSelection d. _getSelectedElement - Reversed isOpaque value - + */ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, -/** @scope SCUI.ContentEditableView.prototype */ { - +/** @scope SCUI.ContentEditableView.prototype */ +{ + /** Value of the HTML inside the body of the iframe. */ value: '', - + /** @private */ valueBindingDefault: SC.Binding.single(), - + /** Set to NO to prevent scrolling in the iframe. - */ + */ allowScrolling: YES, - + /** Set to NO when the view needs to be transparent. */ @@ -90,118 +99,126 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, Current selected content in the iframe. */ selection: '', - + /** Read-only value The currently selected image */ selectedImage: null, - + /** Read-only value The currently selected hyperlink - */ + */ selectedHyperlink: null, - + + /** + Read-only value + The currently selected hyperlink + */ + + selectedText: null, + /** A view can be passed that grows/shrinks in dimensions as the ContentEditableView changes dimensions. - */ + */ attachedView: null, - + /** Read-only value OffsetWidth of the body of the iframe. */ offsetWidth: null, - + /** Read-only value. OffsetHeight of the body of the iframe. */ offsetHeight: null, - + /** Set to NO to allow dimensions of the view to change according to the HTML. */ hasFixedDimensions: YES, - + /** A set of values to be applied to the editor when it loads up. Styles can be dashed or camelCase, both are acceptable. - + For example, - + { 'color': 'blue', 'background-color': 'red' } - + OR - + { 'color': 'orange', 'backgroundColor': 'green' } */ inlineStyle: {}, - + /** If set to YES, then HTML from iframe will be saved everytime isEditing is set to YES - */ + */ autoCommit: NO, - + /** Set to NO to prevent automatic cleaning of text inserted into editor */ cleanInsertedText: YES, - + /** Replaces '\n' with ' ' and '\r' with ' ' */ encodeNewLine: NO, - + /** CSS to style the edit content */ styleSheetCSS: '', - + /** An array of link strings. Each string is expected to be a fully formed link tag, eg. - + '' */ styleSheetsLinks: [], - + /** List of menu options to display on right click */ - rightClickMenuOptionsWithoutSelection: [], - - /** + rightClickMenuOptionsWithoutSelection: [], + + /** List of menu options to display on right click with selection */ - rightClickMenuOptionsWithSelection: [], - - docType: '', - - /* + rightClickMenuOptionsWithSelection: [], + + docType: '', + + /* returns right click menu options */ - rightClickMenuOptions: function(){ + rightClickMenuOptions: function() { //get var ret = []; - var wos = this.get('rightClickMenuOptionsWithoutSelection'), ws = this.get('rightClickMenuOptionsWithSelection'); - if(this.get('selection') && this.get('selection').length > 0){ - ws.forEach(function(j){ + var wos = this.get('rightClickMenuOptionsWithoutSelection'), + ws = this.get('rightClickMenuOptionsWithSelection'); + if (this.get('selectedText') && this.get('selectedText').length > 0) { + ws.forEach(function(j) { ret.pushObject(j); }); } - wos.forEach(function(i){ + wos.forEach(function(i) { ret.pushObject(i); }); return ret; - }.property('rightClickMenuOptionsWithoutSelection', 'rightClickMenuOptionsWithSelection', 'selection').cacheable(), - /** + }.property('rightClickMenuOptionsWithoutSelection', 'rightClickMenuOptionsWithSelection', 'selection').cacheable(), + /** Used specifically for encoding special characters in an anchor tag's href attribute. This is mostly an edge case. (<) - %3C @@ -210,67 +227,65 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, (&) - & (') - %27 */ - encodeContent: YES, - - /** + encodeContent: YES, + + /** Tab options */ - indentOnTab: YES, - tabSize: 2, + indentOnTab: YES, + tabSize: 2, - /* + /* receives actions on click to insert event... */ - insertTarget: null, + insertTarget: null, + + /* + permits right-click menu when no menu options provided + */ + allowsDefaultRightClickMenu: YES, + + isFocused: NO, - isFocused: NO, - selectionSaved: NO, - - displayProperties: ['value'], - - render: function(context, firstTime) { + + displayProperties: ['value'], + + render: function(context, firstTime) { var value = this.get('value'); var isOpaque = !this.get('isOpaque'); - var allowScrolling = this.get('allowScrolling') ? 'yes' : 'no'; - var frameBorder = isOpaque ? '0' : '1'; + var allowScrolling = this.get('allowScrolling') ? 'yes': 'no'; + var frameBorder = isOpaque ? '0': '1'; var styleString = 'position: absolute; width: 100%; height: 100%; border: 0px; margin: 0px; padding: 0p;'; - + if (firstTime) { - context.push( '' ); - + context.push(''); } else if (this._document) { var html = this._document.body.innerHTML; - + if (this.get('encodeContent')) { html = this._encodeValues(html); } - - if(this.get('encodeNewLine')){ + + if (this.get('encodeNewLine')) { html = html.replace(/\r/g, ' '); html = html.replace(/\n/g, ' '); } - + if (value !== html) { this._document.body.innerHTML = value; - } } }, - + didCreateLayer: function() { sc_super(); var f = this.$('iframe'); SC.Event.add(f, 'load', this, this.editorSetup); }, - + displayDidChange: function() { var doc = this._document; if (doc) { @@ -278,11 +293,25 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } sc_super(); }, - - willDestroyLayer: function() { - var doc = this._document; - var docBody = doc.body; - + + _attachEventHandlers: function(doc, docBody) { + SC.Event.add(docBody, 'focus', this, this.bodyDidFocus); + SC.Event.add(docBody, 'blur', this, this.bodyDidBlur); + SC.Event.add(docBody, 'mouseup', this, this.mouseUp); + SC.Event.add(docBody, 'keyup', this, this.keyUp); + SC.Event.add(docBody, 'paste', this, this.paste); + SC.Event.add(docBody, 'dblclick', this, this.doubleClick); + if (this.get('indentOnTab')) SC.Event.add(docBody, 'keydown', this, this.keyDown); + // there are certian cases where the body of the iframe won't have focus + // but the user will be able to type... this happens when the user clicks on + // the white space where there's no content. This event handler + // ensures that the body will receive focus when the user clicks on that area. + SC.Event.add(doc, 'click', this, this.focus); + SC.Event.add(doc, 'mouseup', this, this.docMouseUp); + SC.Event.add(doc, 'contextmenu', this, this.contextmenu); + }, + + _removeEventHandlers: function(doc, docBody) { if (this.get('indentOnTab')) SC.Event.remove(docBody, 'keydown', this, this.keyDown); SC.Event.remove(docBody, 'focus', this, this.bodyDidFocus); SC.Event.remove(docBody, 'blur', this, this.bodyDidBlur); @@ -294,11 +323,16 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, SC.Event.remove(this.$('iframe'), 'load', this, this.editorSetup); SC.Event.remove(doc, 'mouseup', this, this.docMouseUp); SC.Event.remove(doc, 'contextmenu', this, this.contextmenu); - + }, + + willDestroyLayer: function() { + var doc = this._document; + var docBody = doc.body; + this._removeEventHandlers(doc, docBody); sc_super(); }, - - editorSetup: function() { + + editorSetup: function() { // store the document property in a local variable for easy access this._iframe = this._getFrame(); this._document = this._getDocument(); @@ -308,14 +342,16 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } var doc = this._document; - + doc.open(); doc.write(this.get('docType')); doc.write(''); - - var styleSheetsLinks = this.get('styleSheetsLinks'), styleSheetLink; + + var styleSheetsLinks = this.get('styleSheetsLinks'), + styleSheetLink; if (styleSheetsLinks.length && styleSheetsLinks.length > 0) { - for (var i = 0, j = styleSheetsLinks.length; i < j ; i++) { + for (var i = 0, + j = styleSheetsLinks.length; i < j; i++) { styleSheetLink = styleSheetsLinks[i]; if (styleSheetLink.match(/\/)) { doc.write(styleSheetsLinks[i]); @@ -325,10 +361,9 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, doc.write(''); doc.close(); - - + var styleSheetCSS = this.get('styleSheetCSS'); - if (!(SC.none(styleSheetCSS) || styleSheetCSS === '')) { + if (! (SC.none(styleSheetCSS) || styleSheetCSS === '')) { var head = doc.getElementsByTagName('head')[0]; if (head) { var el = doc.createElement("style"); @@ -339,21 +374,22 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } else { el.innerHTML = styleSheetCSS; } - el = head = null; //clean up memory + el = head = null; + //clean up memory } } - + // set contentEditable to true... this is the heart and soul of the editor var value = this.get('value'); var docBody = doc.body; docBody.contentEditable = true; - + if (!SC.browser.msie) { doc.execCommand('styleWithCSS', false, false); } - + if (!this.get('isOpaque')) { - docBody.style.background = 'transparent'; + docBody.style.background = 'transparent'; // the sc-web-view adds a gray background to the WebView... removing in the // case isOpaque is NO this.$().setClass('sc-web-view', NO); @@ -362,111 +398,98 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, var inlineStyle = this.get('inlineStyle'); var docBodyStyle = this._document.body.style; for (var key in inlineStyle) { - if (inlineStyle.hasOwnProperty(key)) { + if (inlineStyle.hasOwnProperty(key)) { docBodyStyle[key.toString().camelize()] = inlineStyle[key]; } } - - docBody.innerHTML = value; - // set min height beyond which ContentEditableView can't shrink if hasFixedDimensions is set to false if (!this.get('hasFixedDimensions')) { var height = this.get('layout').height; if (height) { this._minHeight = height; } - + var width = this.get('layout').width; if (width) { this._minWidth = width; } } - // attach the required events - SC.Event.add(docBody, 'focus', this, this.bodyDidFocus); - SC.Event.add(docBody, 'blur', this, this.bodyDidBlur); - SC.Event.add(docBody, 'mouseup', this, this.mouseUp); - SC.Event.add(docBody, 'keyup', this, this.keyUp); - SC.Event.add(docBody, 'paste', this, this.paste); - SC.Event.add(docBody, 'dblclick', this, this.doubleClick); - if (this.get('indentOnTab')) SC.Event.add(docBody, 'keydown', this, this.keyDown); - // there are certian cases where the body of the iframe won't have focus - // but the user will be able to type... this happens when the user clicks on - // the white space where there's no content. This event handler - // ensures that the body will receive focus when the user clicks on that area. - SC.Event.add(doc, 'click', this, this.focus); - SC.Event.add(doc, 'mouseup', this, this.docMouseUp); - SC.Event.add(doc, 'contextmenu', this, this.contextmenu); - + this._attachEventHandlers(doc, docBody); + // call the SC.WebView iframeDidLoad function to finish setting up this.iframeDidLoad(); this.focus(); - - // When body.innerHTML is used to insert HTML into the iframe, it results in a bug + + // When body.innerHTML is used to insert HTML into the iframe, it results in a bug // (if you select-all then try and delete, it won't have any effect). This // is a hack for that problem doc.execCommand('inserthtml', false, ' '); doc.execCommand('undo', false, null); }, - - bodyDidFocus: function (evt) { + + bodyDidFocus: function(evt) { this.set('isFocused', YES); - + }, - - bodyDidBlur: function (evt) { + + bodyDidBlur: function(evt) { this.set('isFocused', NO); }, - - contextmenu: function(evt) { - var menuOptions = this.get('rightClickMenuOptions'); - var numOptions = menuOptions.get('length'); - - if (menuOptions.length > 0) { - - var pane = this.contextMenuView.create({ - defaultResponder: this.get('rightClickMenuDefaultResponder'), - layout: { width: 200}, - itemTitleKey: 'title', - itemTargetKey: 'target', - itemActionKey: 'action', - itemSeparatorKey: 'isSeparator', - itemIsEnabledKey: 'isEnabled', - items: menuOptions - }); - - pane.popup(this, evt); - - if (evt.preventDefault) { + + contextmenu: function(evt) { + var menuOptions = this.get('rightClickMenuOptions'); + var numOptions = menuOptions.get('length'); + + if (menuOptions.length > 0) { + + var pane = this.contextMenuView.create({ + defaultResponder: this.get('rightClickMenuDefaultResponder'), + layout: { + width: 200 + }, + itemTitleKey: 'title', + itemTargetKey: 'target', + itemActionKey: 'action', + itemSeparatorKey: 'isSeparator', + itemIsEnabledKey: 'isEnabled', + items: menuOptions + }); + + pane.popup(this, evt); + } + + if ((menuOptions.length > 0) || (!this.get('allowsDefaultRightClickMenu'))) { + if (evt.preventDefault) { evt.preventDefault(); - } else { + } else { evt.stop(); } evt.returnValue = false; evt.stopPropagation(); return NO; } - }, - + }, + // Can't do this on the body mouseup function (The body mouse // function is not always triggered, e.g, when the mouse cursor is behind // a border) - docMouseUp: function(evt) { + docMouseUp: function(evt) { var that = this; - this.invokeLast(function() { - var image = that.get('selectedImage'); - if (image) { - image.style.width = image.width + 'px'; - image.style.height = image.height + 'px'; - that.set('isEditing', YES); - } - }); - }, - - /** + this.invokeLast(function() { + var image = that.get('selectedImage'); + if (image) { + image.style.width = image.width + 'px'; + image.style.height = image.height + 'px'; + that.set('isEditing', YES); + } + }); + }, + + /** Override this method to execute an action on double click. This was done this way (as opposed to passing target/action) to be able to pass down the evt parameter to the event handler. @@ -478,22 +501,22 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, // do your thing... SC.RunLoop.end(); }, - - contextMenuView: SCUI.ContextMenuPane.extend({ - popup: function(anchorView, evt) { + + contextMenuView: SCUI.ContextMenuPane.extend({ + popup: function(anchorView, evt) { if ((!anchorView || !anchorView.isView) && !this.get('usingStaticLayout')) return NO; - + var anchor = anchorView.isView ? anchorView.get('layer') : anchorView; // prevent the browsers context meuns (if it has one...). (SC does not handle oncontextmenu event.) document.oncontextmenu = function(e) { var menuOptions = anchorView.get('rightClickMenuOptions'); - var numOptions = menuOptions.get('length'); + var numOptions = menuOptions.get('length'); - if (menuOptions.length > 0) { + if (menuOptions.length > 0) { if (evt.preventDefault) { evt.preventDefault(); - } else { + } else { evt.stop(); } evt.returnValue = false; @@ -501,70 +524,81 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, return false; } }; - + // Popup the menu pane this.beginPropertyChanges(); var it = this.get('displayItems'); - this.set('anchorElement', anchor) ; + this.set('anchorElement', anchor); this.set('anchor', anchorView); - this.set('preferType', SC.PICKER_MENU) ; + this.set('preferType', SC.PICKER_MENU); this.endPropertyChanges(); - - return arguments.callee.base.base.apply(this,[anchorView, [evt.pageX + 5, evt.pageY + 5, 1]]); +// TODO [JS]: this is putting the pop-up at quite a distance from the click, OR-7463 + return arguments.callee.base.base.apply(this, [anchorView, [evt.pageX + 5, evt.pageY + 5, 1]]); }, - + exampleView: SC.MenuItemView.extend({ renderLabel: function(context, label) { if (this.get('escapeHTML')) { - label = SC.RenderContext.escapeHTML(label) ; + label = SC.RenderContext.escapeHTML(label); } - context.push(""+label+"") ; + context.push("" + label + ""); } }) - - }), + + }), keyUp: function(event) { SC.RunLoop.begin(); switch (SC.FUNCTION_KEYS[event.keyCode]) { - case 'left': - case 'right': - case 'up': - case 'down': - case 'return': - this.querySelection(); - break; - } - + case 'left': + case 'right': + case 'up': + case 'down': + case 'return': + this.querySelection(); + break; + } + // [JS] control-A on windows/linux, or cmd-a on mac, selects all automatically, and should update selection + // jquery hack send meta AND ctrl if ctrl was pressed, which means we'd get triggered on ctrl even if select-all didn't happen, so look for meta without control + // complaint reported in 2008, but it is still there in jquery now, and in sproutcore's corequery base + if (event.keyCode === 65 && ((SC.browser.mac && event.metaKey && !event.ctrlKey) || (!SC.browser.mac && event.ctrlKey))) { + this.querySelection(); + } + if (!this.get('hasFixedDimensions')) { this.invokeLast(this._updateLayout); } this.set('isEditing', YES); - + SC.RunLoop.end(); }, - - keyDown: function(event) { - SC.RunLoop.begin(); - - var tabSize = this.get('tabSize'); + + _tabKeyDown: function(event) { + // insert spaces instead of actual tab character + var tabSize = this.get('tabSize'), + spaces = []; if (SC.typeOf(tabSize) !== SC.T_NUMBER) { // tabSize is not a number. Bail out and recover gracefully return; } - - var spaces = []; + for (var i = 0; i < tabSize; i++) { spaces.push('\u00a0'); } - - if (SC.FUNCTION_KEYS[event.keyCode] === 'tab') { - event.preventDefault(); - this.insertHTML(spaces.join(''), NO); + + event.preventDefault(); + this.insertHTML(spaces.join(''), NO); + }, + + keyDown: function(event) { + SC.RunLoop.begin(); + if ((SC.FUNCTION_KEYS[event.keyCode] === 'tab') && this.get('indentOnTab')) { + this._tabKeyDown(event); } - + if (SC.browser.msie) { + // IE workaround - return key might do the wrong thing var element = this._getSelectedElement(); if (SC.FUNCTION_KEYS[event.keyCode] === 'return' && element.nodeName !== 'LI') { @@ -575,34 +609,33 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, event.preventDefault(); } } - + SC.RunLoop.end(); }, mouseUp: function(evt) { this._mouseUp = true; SC.RunLoop.begin(); - if(this.get('insertInProgress')){ + if (this.get('insertInProgress')) { this.set('insertInProgress', NO); this.get('insertTarget').sendAction('insert'); } this.querySelection(); - + //attempting to help webkit select images... - if(evt.target && evt.target.nodeName === "IMG"){ + if (evt.target && evt.target.nodeName === "IMG") { var sel = this._iframe.contentWindow.getSelection(), - range = this._iframe.contentWindow.document.createRange(); - + range = this._iframe.contentWindow.document.createRange(); + range.selectNode(evt.target); sel.removeAllRanges(); sel.addRange(range); - } - - + } + if (!this.get('hasFixedDimensions')) { this.invokeLast(this._updateLayout); } - + this.set('isEditing', YES); SC.RunLoop.end(); }, @@ -615,20 +648,20 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, this.invokeLast(this._updateLayout); } this.set('isEditing', YES); - + SC.RunLoop.end(); return YES; }, - + /** @property String */ frameName: function() { - return this.get('layerId') + '_frame' ; + return this.get('layerId') + '_frame'; }.property('layerId').cacheable(), - + editorHTML: function(key, value) { var doc = this._document; if (!doc) return NO; - + if (value !== undefined) { doc.body.innerHTML = value; return YES; @@ -640,165 +673,130 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } } }.property(), - - selectionIsBold: function(key, val) { - var editor = this._document ; + + selectionRange: function() { + var selection = this.get('selection'), + range = null; + if (SC.none(selection)) { + return null; + } + if (SC.browser.msie) { + range = selection.createRange(); + } else { + // *should* never be 0 if there's a selection active + range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; + } + return range; + }.property('selection'), + + selectionPlainText: function() { + var selection = this.get('selection'); + return SC.none(selection) ? '': selection.toString(); + }.property('selection').cacheable(), + + _selectionIsSomething: function(key, val, something) { + var editor = this._document; if (!editor) return NO; - + if (val !== undefined) { - if (editor.execCommand('bold', false, val)) { + if (editor.execCommand(something, false, val)) { this.set('isEditing', YES); } } - - return this._document.queryCommandState('bold'); + + return editor.queryCommandState(something); + }, + + selectionIsBold: function(key, val) { + return this._selectionIsSomething(key, val, 'bold'); }.property('selection').cacheable(), - + selectionIsItalicized: function(key, val) { - var doc = this._document ; - if (!doc) return NO; - - if (val !== undefined) { - if (doc.execCommand('italic', false, val)) { - this.set('isEditing', YES); - } - } - - return doc.queryCommandState('italic'); + return this._selectionIsSomething(key, val, 'italic'); }.property('selection').cacheable(), - + selectionIsUnderlined: function(key, val) { - var doc = this._document ; - if (!doc) return NO; - - if (val !== undefined) { - if (doc.execCommand('underline', false, val)) { - this.set('isEditing', YES); - } - } - - return doc.queryCommandState('underline'); + return this._selectionIsSomething(key, val, 'underline'); }.property('selection').cacheable(), - - // FIXME: [MT] queryCommandState('justifyXXXX') always returns fasle in safari... - // find a workaround - selectionIsCenterJustified: function(key, val) { - var doc = this._document ; + + selectionIsStrikedThrough: function(key, val) { + return this._selectionIsSomething(key, val, 'strikeThrough'); + }.property('selection').cacheable(), + + _selectionIsJustified: function(key, val, justify) { + var doc = this._document; if (!doc) return NO; - + if (val !== undefined) { if (SC.browser.msie) { - this._alignContentForIE('center'); + this._alignContentForIE(justify); } else { - doc.execCommand('justifycenter', false, val); + doc.execCommand(justify, false, val); } - + // since DOM is significantly altered, selection needs to be refreshed this.querySelection(); this.set('isEditing', YES); } - - return doc.queryCommandState('justifycenter'); + // FIXME: [JS] queryCommandState('justifyXXXX') always returns false in safari... + return doc.queryCommandState(justify); + }, + + selectionIsCenterJustified: function(key, val) { + this._selectionIsJustified(key, val, 'justifycenter'); }.property('selection').cacheable(), - + selectionIsRightJustified: function(key, val) { - var doc = this._document ; - if (!doc) return NO; - - if (val !== undefined) { - if (SC.browser.msie) { - this._alignContentForIE('right'); - } else { - doc.execCommand('justifyright', false, val); - } - - this.querySelection(); - this.set('isEditing', YES); - } - - return doc.queryCommandState('justifyright'); + this._selectionIsJustified(key, val, 'justifyright'); }.property('selection').cacheable(), - + selectionIsLeftJustified: function(key, val) { - var doc = this._document ; - if (!doc) return NO; - - if (val !== undefined) { - if (SC.browser.msie) { - this._alignContentForIE('left'); - } else { - doc.execCommand('justifyleft', false, val); - } - - this.querySelection(); - this.set('isEditing', YES); - } - - return doc.queryCommandState('justifyleft'); + this._selectionIsJustified(key, val, 'justifyleft'); }.property('selection').cacheable(), - - selectionIsFullJustified: function(key, val) { - var doc = this._document ; - if (!doc) return NO; - - if (val !== undefined) { - if (SC.browser.msie) { - this._alignContentForIE('justify'); - } else { - doc.execCommand('justifyfull', false, val); - } - this.querySelection(); - this.set('isEditing', YES); - } - - return doc.queryCommandState('justifyfull'); + selectionIsFullJustified: function(key, val) { + this._selectionIsJustified(key, val, 'justifyfull'); }.property('selection').cacheable(), - - // TODO: [MT] - Clean some of this code up - _alignContentForIE: function(alignment) { - var doc = this._document ; + + // TODO: [JS] - Clean some of this code up + _alignContentForIE: function(justify) { + var doc = this._document; var elem = this._getSelectedElement(); var range = doc.selection.createRange(); var html, newHTML; - + var alignment; + switch (justify) { + case 'justifycenter': + alignment = 'center'; + break; + case 'justifyleft': + alignment = 'left'; + break; + case 'justifyright': + alignment = 'right'; + break; + case 'justifyfull': + alignment = 'justify'; + break; + } + // if it's an image, use the native execcommand for alignment for consistent // behaviour with FF if (elem.nodeName === 'IMG') { - switch (alignment) { - case 'center': - doc.execCommand('justifycenter', false, null); - break; - case 'left': - doc.execCommand('justifyleft', false, null); - break; - case 'right': - doc.execCommand('justifyright', false, null); - break; - case 'justify': - doc.execCommand('justifyfull', false, null); - break; - } - } else if (elem.nodeName !== 'DIV') { + doc.execCommand(justify, false, null); + } else if (elem.nodeName !== 'DIV' || elem.innerText !== range.text) { html = range.htmlText; newHTML = '
%@
'.fmt(alignment, alignment, html); range.pasteHTML(newHTML); } else { - if (elem.innerText !== range.text) { - html = range.htmlText; - newHTML = '
%@
'.fmt(alignment, alignment, html); - range.pasteHTML(newHTML); - } else { - elem.style.textAlign = alignment; - elem.align = alignment; - - } + elem.style.textAlign = alignment; + elem.align = alignment; } }, - + selectionIsOrderedList: function(key, val) { - var doc = this._document ; + var doc = this._document; if (!doc) return NO; - + if (val !== undefined) { if (SC.browser.msie && val === YES) { this._createListForIE('ol'); @@ -809,15 +807,15 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } this.set('isEditing', YES); } - + return doc.queryCommandState('insertorderedlist'); }.property('selection').cacheable(), - + selectionIsUnorderedList: function(key, val) { - var doc = this._document ; + var doc = this._document; if (!doc) return NO; - - if (val !== undefined) { + + if (val !== undefined) { if (SC.browser.msie && val === YES) { this._createListForIE('ul'); } else { @@ -827,22 +825,22 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } this.set('isEditing', YES); } - + return doc.queryCommandState('insertunorderedlist'); }.property('selection').cacheable(), - + _createListForIE: function(tag) { var html = ''; - var doc = this._document ; + var doc = this._document; var range = this._iframe.document.selection.createRange(); var text = range.text; var textArray = text.split('\n'); var elem = this._getSelectedElement(); - + if (elem.nodeName === 'LI') { elem = elem.parentNode; } - + if (elem.nodeName === 'OL' || elem.nodeName === 'UL') { var newEl = doc.createElement(tag); newEl.innerHTML = elem.innerHTML; @@ -863,107 +861,90 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, // indent/outdent have some sort of problem with every // browser. Check, - // + // // http://www.quirksmode.org/dom/execCommand.html - // + // // I would avoid using these for now and go with // indentOnTab selectionIsIndented: function(key, val) { - var doc = this._document ; + var doc = this._document; if (!doc) return NO; - + if (val !== undefined) { if (doc.execCommand('indent', false, val)) { this.set('isEditing', YES); } } - + if (SC.browser.msie) { return doc.queryCommandState('indent'); } else { /* - [MT] - Buggy... commeting out for now - var elem = this._getSelectedElemented(); - if (!SC.none(elem)) { - if (elem.style['marginLeft'] !== '') { - return YES; - } - } - */ + [MT] - Buggy... commeting out for now + var elem = this._getSelectedElemented(); + if (!SC.none(elem)) { + if (elem.style['marginLeft'] !== '') { + return YES; + } + } + */ return NO; } }.property('selection').cacheable(), - + selectionIsOutdented: function(key, val) { - var doc = this._document ; + var doc = this._document; if (!doc) return NO; - + if (val !== undefined) { if (doc.execCommand('outdent', false, val)) { this.set('isEditing', YES); } } - + if (SC.browser.msie) { return doc.queryCommandState('outdent'); } else { /* - [MT] - Buggy... commeting out for now - var elem = this._getSelectedElemented(); - if (!SC.none(elem)) { - if (elem.style['marginLeft'] === '') { - return YES; - } - } - */ + [MT] - Buggy... commeting out for now + var elem = this._getSelectedElemented(); + if (!SC.none(elem)) { + if (elem.style['marginLeft'] === '') { + return YES; + } + } + */ return NO; } }.property('selection').cacheable(), - - selectionIsSubscript: function(key, val) { - var doc = this._document ; - if (!doc) return NO; - - if (val !== undefined) { - if (doc.execCommand('subscript', false, val)) { - this.set('isEditing', YES); - } - } - return doc.queryCommandState('subscript'); + selectionIsSubscript: function(key, val) { + return this._selectionIsSomething(key, val, 'subscript'); }.property('selection').cacheable(), - - selectionIsSuperscript: function(key, val) { - var doc = this._document ; - if (!doc) return NO; - - if (val !== undefined) { - if (doc.execCommand('superscript', false, val)) { - this.set('isEditing', YES); - } - } - return doc.queryCommandState('superscript'); + selectionIsSuperscript: function(key, val) { + return this._selectionIsSomething(key, val, 'superscript'); }.property('selection').cacheable(), - + selectionFontName: function(key, val) { - var doc = this._document ; + var doc = this._document; if (!doc) return ''; var ret; - + if (val !== undefined) { - var identifier = '%@%@'.fmt(this.get('layerId'), '-ce-font-temp'); - + var identifier = '%@%@'.fmt(this.get('layerId'), '-ce-font-temp'); + if (doc.execCommand('fontname', false, identifier)) { var fontTags = doc.getElementsByTagName('font'); - for (var i = 0, j = fontTags.length; i < j; i++) { + for (var i = 0, + j = fontTags.length; i < j; i++) { var fontTag = fontTags[i]; if (fontTag.face === identifier) { fontTag.face = ''; fontTag.style.fontFamily = val; } } - + this.set('isEditing', YES); } } else { @@ -976,22 +957,23 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, return ret; } }.property('selection').cacheable(), - + selectionFontSize: function(key, value) { var frame = this._iframe; var doc = this._document; if (!doc) return ''; var ret; - + if (value !== undefined) { var identifier = '%@%@'.fmt(this.get('layerId'), '-ce-size-temp'); - + // apply unique string to font size to act as identifier if (doc.execCommand('fontname', false, identifier)) { // get all newly created font tags var fontTags = doc.getElementsByTagName('font'); - for (var i = 0, j = fontTags.length; i < j; i++) { + for (var i = 0, + j = fontTags.length; i < j; i++) { var fontTag = fontTags[i]; if (fontTag.face === identifier) { fontTag.face = ''; @@ -1004,14 +986,14 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } else { var elm = this._findFontTag(this._getSelectedElement()); if (elm && elm.nodeName.toLowerCase() === 'font') { - ret = elm.style.fontSize; + ret = elm.style.fontSize; } else { ret = null; } return ret; } }.property('selection').cacheable(), - + _findFontTag: function(elem) { while (elem.nodeName !== 'BODY') { if (elem.nodeName === 'FONT') { @@ -1021,14 +1003,14 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } } }, - + selectionFontColor: function(key, value) { if (!this.get('isVisibleInWindow')) return ''; - - var doc = this._document ; + + var doc = this._document; if (!doc) return ''; - - if (value !== undefined) { + + if (value !== undefined) { if (this.get('selectionSaved') === YES) { this.restoreSelection(); } @@ -1038,7 +1020,7 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, this._last_font_color_cache = value; } } - + if (this._last_font_color_cache) { return this._last_font_color_cache; } else { @@ -1048,38 +1030,44 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, return this._last_font_color_cache; } } - + return ''; }.property('selection').cacheable(), - + selectionBackgroundColor: function(key, value) { if (!this.get('isVisibleInWindow')) return ''; - - var doc = this._document ; + + var doc = this._document; if (!doc) return ''; - - var prop; - if (SC.browser.msie) prop = 'backcolor'; - else prop = 'hilitecolor'; - if (!SC.browser.msie) doc.execCommand('styleWithCSS', false, true); + var prop = SC.browser.msie ? 'backcolor': 'hilitecolor'; + if (!SC.browser.msie) { + doc.execCommand('styleWithCSS', false, true); + } if (value !== undefined) { if (this.get('selectionSaved') === YES) { this.restoreSelection(); } + // TODO: this sets it on the whole DIV block, if the object is inside a DIV for alignment reasons + // fix by inserting a span into the div around the div's contents, if the selection is only one div element + // setting THAT to the selection, and then execute the command. ick. + // BTW, this is a BUG in FF, where the spec says it should NOT update the whole div + // only do this on FF because WebKit gets it right. if (doc.execCommand(prop, false, value)) { this.saveSelection(); this.set('isEditing', YES); this._last_background_color_cache = value; } } - + if (this._last_background_color_cache) { return this._last_background_color_cache; } else { var color = doc.queryCommandValue(prop); - if (!SC.browser.msie) doc.execCommand('styleWithCSS', false, false); + if (!SC.browser.msie) { + doc.execCommand('styleWithCSS', false, false); + } if (color !== 'transparent') { color = SC.browser.msie ? this.convertBgrToHex(color) : SC.parseColor(color); if (color) { @@ -1088,40 +1076,36 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } } } - + return ''; }.property('selection').cacheable(), - + hyperlinkValue: function(key, value) { var hyperlink = this.get('selectedHyperlink'); if (!hyperlink) return ''; - + if (!SC.none(value)) { hyperlink.href = value; this.set('isEditing', YES); return value; - } else { return hyperlink.href; - } }.property('selectedHyperlink').cacheable(), - + hyperlinkHoverValue: function(key, value) { var hyperlink = this.get('selectedHyperlink'); if (!hyperlink) return ''; - + if (value !== undefined) { hyperlink.title = value; this.set('isEditing', YES); return value; - } else { return hyperlink.title; - } }.property('selectedHyperlink').cacheable(), - + /** imageAlignment doesn't need to be updated everytime the selection changes... only when the current selection is an image @@ -1129,197 +1113,207 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, imageAlignment: function(key, value) { var image = this.get('selectedImage'); if (!image) return ''; - + if (value !== undefined) { image.align = value; this.set('isEditing', YES); return value; - - } else { + + } else { return image.align; - + } }.property('selectedImage').cacheable(), - + imageWidth: function(key, value) { var image = this.get('selectedImage'); if (!image) return ''; - + if (value !== undefined) { this.set('isEditing', YES); - image.width = value*1; - image.style.width = value+"px"; + image.width = value * 1; + image.style.width = value + "px"; return value; - - } else { + + } else { return image.clientWidth; - + } }.property('selectedImage').cacheable(), - + imageHeight: function(key, value) { var image = this.get('selectedImage'); if (!image) return ''; - + if (value !== undefined) { this.set('isEditing', YES); - image.height = value*1; - image.style.height = value+"px"; + image.height = value * 1; + image.style.height = value + "px"; return value; - - } else { + + } else { return image.clientHeight; - + } }.property('selectedImage').cacheable(), - + imageDescription: function(key, value) { var image = this.get('selectedImage'); if (!image) return ''; - + if (value !== undefined) { this.set('isEditing', YES); image.title = value; image.alt = value; return value; - - } else { + + } else { return image.alt; - + } }.property('selectedImage').cacheable(), - + imageBorderWidth: function(key, value) { var image = this.get('selectedImage'); if (!image) return ''; - + if (value !== undefined) { this.set('isEditing', YES); image.style.borderWidth = value; return value; - - } else { + + } else { return image.style.borderWidth; - + } }.property('selectedImage').cacheable(), - + imageBorderColor: function(key, value) { var image = this.get('selectedImage'); if (!image) return ''; - + if (value !== undefined) { this.set('isEditing', YES); - + image.style.borderColor = value; return value; - - } else { + + } else { var color = image.style.borderColor; if (color !== '') { return SC.parseColor(color); } else { return ''; } - + } }.property('selectedImage').cacheable(), - + imageBorderStyle: function(key, value) { var image = this.get('selectedImage'); if (!image) return ''; - + if (value !== undefined) { this.set('isEditing', YES); image.style.borderStyle = value; return value; - - } else { + + } else { return image.style.borderStyle; - + + } + }.property('selectedImage').cacheable(), + + imageSource: function(key, value) { + var image = this.get('selectedImage'); + if (!image) return ''; + + if (value !== undefined) { + image.src = value; + this.set('isEditing', YES); + return value; + + } else { + return image.src; + } }.property('selectedImage').cacheable(), - + resetImageDimensions: function() { var image = this.get('selectedImage'); if (!image) return NO; - + image.style.width = ''; image.style.height = ''; image.removeAttribute('width'); image.removeAttribute('height'); - + this.set('isEditing', YES); this.notifyPropertyChange('selectedImage'); - + return image; }, - focus: function(){ + focus: function() { if (!SC.none(this._document)) { this._document.body.focus(); - this.set('selection', ''); this.querySelection(); } }, - + querySelection: function() { - var frame = this._iframe; - if (!frame) return; - - var selection = ''; - if (SC.browser.msie) { - selection = this._iframe.document.selection.createRange().text; - if (SC.none(selection)) { - selection = ''; - } - } else { - var frameWindow = frame.contentWindow; - selection = frameWindow.getSelection(); - } - + var selection = this._getSelection(); + this._resetColorCache(); - + + // The DOM actually only has one selection object (per document) that never really changes, so + // SproutCore's detection of whether or not the selection changed won't actually work - the object is the same + // hence, why this code explicitly calls will and did change this.propertyWillChange('selection'); - this.set('selection', selection.toString()); + this.set('selection', selection); this.propertyDidChange('selection'); }, - + canCreateLink: function() { - var selection = this.get('selection'); - return selection && selection.length > 0 ? YES : NO; - }.property('selection'), - + var selectedText = this.get('selectedText'); + var rv = (selectedText && selectedText.length > 0) || !SC.none(this.get('selectedImage')); + return rv; + }.property('selectedText', 'selectedImage'), + createLink: function(value) { var doc = this._document; var frame = this._iframe; - if (!(doc && frame)) return NO; + if (! (doc && frame)) return NO; if (SC.none(value) || value === '') return NO; - + /* HACK: [MT] - This is an interesting hack... The problem with execCommand('createlink') is it only tells you if hyperlink creation was successful... it doesn't return the hyperlink that was created. - + To counter this problem, I'm creating a random string and assigning it as the href. If the frame.contentWindow.getSelection() method fails, I iterate over the children of the currently selected node and find the anchor tag with the crazy url and assign it as the currently selected hyperlink, after which I do a bit of cleanup and set value to the href property. + + JWS: why didn't you just set it to the value uri up front and just look for that...well, i suppose that one could + already exist elsewhere in the doc...then again, this code doesn't do what you describe in any case... + + i'm going to just assign it the base url and then requery the selection and see what happens... */ - - var radomUrl = '%@%@%@%@%@'.fmt('http://', - this.get('frameName'), - new Date().getTime(), - parseInt(Math.random()*100000, 0), - '.com/'); - + /* + var radomUrl = '%@%@%@%@%@'.fmt('http://', this.get('frameName'), new Date().getTime(), parseInt(Math.random() * 100000, 0), '.com/'); + if (doc.execCommand('createlink', false, radomUrl)) { - var aTags = doc.getElementsByTagName('A'), hyperlink, child; - - for (var x = 0, y = aTags.length; x < y; x++) { + var aTags = doc.getElementsByTagName('A'), + hyperlink, + child; + + for (var x = 0, + y = aTags.length; x < y; x++) { child = aTags[x]; if (child.href === radomUrl) { @@ -1328,52 +1322,110 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } } } - + if (hyperlink) { hyperlink.href = value; this.set('selectedHyperlink', hyperlink); this.set('isEditing', YES); return YES; - + + } else { + return NO; + + } + */ + + + if (!this.get('selectedText').length && !this.get('selectedImage')) { + return NO; + } + + if (doc.execCommand('createlink', false, value)) { + this.querySelection(); + this.set('isEditing', YES); + return YES; } else { return NO; - } + + }, + + _removeLinkCompletely: function(doc) { + // taken from an older DOJO's editor code + var selection = this.get('selection'); + var selectionRange = selection.getRangeAt(0); + var selectionStartContainer = selectionRange.startContainer; + var selectionStartOffset = selectionRange.startOffset; + var selectionEndContainer = selectionRange.endContainer; + var selectionEndOffset = selectionRange.endOffset; + + // select our link and unlink + var range = doc.createRange(); + var a = this._getSelectedElement(); + while (a.nodeName != "A") { a = a.parentNode; } + range.selectNode(a); + selection.removeAllRanges(); + selection.addRange(range); + + var returnValue = doc.execCommand("unlink", false, null); + + // restore original selection + /* + // FIXME [JS] - this doesn't work when the link is around an IMG (and nothing else + var selectionRange = doc.createRange(); + selectionRange.setStart(selectionStartContainer, selectionStartOffset); + selectionRange.setEnd(selectionEndContainer, selectionEndOffset); + selection.removeAllRanges(); + selection.addRange(selectionRange); + */ + // right now, with that commented out, the selection will likely clear after an unlink + // in some oddball cases, it MAY end up selecting more + this.querySelection(); + this.set('isEditing', YES); + + return returnValue; }, - + removeLink: function() { var doc = this._document; if (!doc) return NO; - + + if (SC.browser.mozilla || SC.browser.chrome) { + // issue - it should unlink, but it only unlinks correctly if you selected the WHOLE link + return this._removeLinkCompletely(doc); + } if (doc.execCommand('unlink', false, null)) { this.set('selectedHyperlink', null); this.set('isEditing', YES); return YES; } - + return NO; }, - + // FIXME: [MT] Should do something similar to what's being done on // image creation (Assigning the newly created image to the selectedImage // property) + // "fixed"? [JS] if no real selection, then selection returns next element + // so if the image is inserted after the cursor, it should be the selectedImage now insertImage: function(value) { var doc = this._document; if (!doc) return NO; if (SC.none(value) || value === '') return NO; - + if (doc.execCommand('insertimage', false, value)) { this.set('isEditing', YES); + this.querySelection(); return YES; } return NO; }, - + /** Inserts a snippet of HTML into the editor at the cursor location. If the editor is not in focus then it appens the HTML at the end of the document. - + @param {String} HTML to be inserted @param {Boolean} Optional boolean to determine if a single white space is to be inserted after the HTML snippet. Defaults to YES. This is enabled to protect @@ -1384,19 +1436,19 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, var doc = this._document; if (!doc) return NO; if (SC.none(value) || value === '') return NO; - + if (SC.none(insertWhiteSpaceAfter) || insertWhiteSpaceAfter) { value += '\u00a0'; } - + if (SC.browser.msie) { if (!this.get('isFocused')) { this.focus(); } - doc.selection.createRange().pasteHTML(value); - this.set('isEditing', YES); + doc.selection.createRange().pasteHTML(value); + this.set('isEditing', YES); return YES; - + } else { if (doc.execCommand('inserthtml', false, value)) { this.set('isEditing', YES); @@ -1405,7 +1457,7 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, return NO; } }, - + /** Inserts a SC view into the editor by first converting the view into html then inserting it using insertHTML(). View objects, classes @@ -1428,14 +1480,14 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, @param {View} SC view to be inserted */ insertView: function(view) { - if(SC.typeOf(view) === SC.T_STRING){ - // if nowShowing was set because the content was set directly, then + if (SC.typeOf(view) === SC.T_STRING) { + // if nowShowing was set because the content was set directly, then // do nothing. - if (view === SC.CONTENT_SET_DIRECTLY) return ; + if (view === SC.CONTENT_SET_DIRECTLY) return; // otherwise, if nowShowing is a non-empty string, try to find it... - if (view && view.length>0) { - if (view.indexOf('.')>0) { + if (view && view.length > 0) { + if (view.indexOf('.') > 0) { view = SC.objectForPropertyPath(view, null); } else { view = SC.objectForPropertyPath(view, this.get('page')); @@ -1453,14 +1505,14 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, var html; if (SC.browser.msie) { - html = '' + context + ''; + html = '' + context + ''; } else { html = '' + context + ''; } this.insertHTML(html); }, - + /** Filters out junk tags when copying/pasting from MS word. This function is called automatically everytime the users paste into the editor. @@ -1495,7 +1547,7 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, html = html.replace(/\s*TEXT-INDENT: 0cm\s*;/gi, ''); html = html.replace(/\s*TEXT-INDENT: 0cm\s*"/gi, "\""); html = html.replace(/\s*PAGE-BREAK-BEFORE: [^\s;]+;?"/gi, "\""); - html = html.replace(/\s*FONT-VARIANT: [^\s;]+;?"/gi, "\"" ); + html = html.replace(/\s*FONT-VARIANT: [^\s;]+;?"/gi, "\""); html = html.replace(/\s*tab-stops:[^;"]*;?/gi, ''); html = html.replace(/\s*tab-stops:[^"]*/gi, ''); @@ -1510,119 +1562,178 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, html = html.replace(/\<(\w[^>]*) onmouseover="([^\"]*)"([^>]*)/gi, "<$1$3"); html = html.replace(/\<(\w[^>]*) onmouseout="([^\"]*)"([^>]*)/gi, "<$1$3"); + // remove xstr non-xml attribute in table tags + html = html.replace(/\<(\w[^>]*) xstr([^>]*)/gi, "<$1$2"); + // remove meta and link tags html = html.replace(/\<(meta|link)[^>]+>\s*/gi, ''); return html; }, - + /** Persists HTML from editor back to value property and sets the isEditing flag to false - + @returns {Boolean} if the operation was successul or not */ commitEditing: function() { var doc = this._document; if (!doc) return NO; - + var value = doc.body.innerHTML; if (this.get('cleanInsertedText')) { value = this.cleanWordHTML(value); } - if(this.get('encodeNewLine')){ + if (this.get('encodeNewLine')) { value = value.replace(/\r/g, ' '); value = value.replace(/\n/g, ' '); } - + if (this.get('encodeContent')) { value = this._encodeValues(value); } - + this.set('value', value); this.set('isEditing', NO); return YES; }, - + /** Selects the current content in editor - - @returns {Boolean} if the operation was successul or not + + @returns {Boolean} if the operation was successful or not */ selectContent: function() { - var doc = this._document; + var doc = this._document, + rv; if (!doc) return NO; - - return doc.execCommand('selectall', false, null); + rv = doc.execCommand('selectall', false, null); + if (rv) { + this.querySelection(); + } + return rv; + }, + + _findAncestor: function(o, tag) { + for (tag = tag.toLowerCase(); o = o.parentNode;) + if (o.tagName && o.tagName.toLowerCase() === tag) { + return o; + } + return null; }, /** Adding an observer that checks if the current selection is an image - or a hyperlink. - */ + or within a hyperlink. + */ selectionDidChange: function() { - var node, - range, - currentImage = null, - currentHyperlink = null; + + //SC.Logger.warn('selectionDidChange'); + + var node, range, currentImage = null, + currentHyperlink = null, + currentText = '', + selection = this.get('selection'); + if (SC.none(selection)) { + this.set('selectedImage', currentImage); + this.set('selectedHyperlink', currentHyperlink); + this.set('selectedText', currentText); + return; + } + range = this.get('selectionRange'); + /* + + The quest for the selected hyperlink is a nasty one, all based on whether or not you just created the link, because + it manipulates the selection range in different ways depending on what was selected. + + If you only selected an image, the IMG is the node unless it is wrapped in an A, in which case the A is the node and you have + to look for its child to get the image. + + If you selected a range of text, then several different things are possible + * if you did not just create a link, then you are in a text node and the link is + ** the range's common ancestor + ** or an ancestor of it + + * if you DID just create a link, the range may be + ** the start/end container (this is what it usually is on Chrome/Webkit) + ** the one and only object between the start and end nodes - this was REALLY tricky + *** it happens to be the next node of the start (which is the previous text block or node) + *** and an ancestor of the end (which is itself the actual content inside the link) + *** but ONLY if those two are the same. otherwise, it'll likely be one of the above cases + + * TODO [JS] still to test/fix: + ** fix link colors (handle with fixing the rest of the firefox color problems) + + BTW if a link failure happened (create link when you shouldn't have, as in there's no text selected or the browser thought there wasn't) + that is a MAJOR KISS YOUR ASS GOODBYE BUG - the content editable ceases to react properly, and selections are just hosed from that point on. + + * TODO [JS] fix image border when image is inside link + when an image is inside a link, the border needs to be set to 0. + HOWEVER, when the border is set to 0, you can't change it to 0 (SC doesn't detect a change) + Workaround set the border to 1, then to 0, and it goes away. + + */ if (SC.browser.msie) { - var selection = this._iframe.document.selection; - range = selection.createRange(); - + // [JS] I'm concerned that this doesn't do "the right thing", but we're not focusing on IE in great detail yet. if (range.length === 1) node = range.item(); - if (range.parentElement) node = range.parentElement(); - - } else { - var targetIframeWindow = this._iframe.contentWindow; - selection = targetIframeWindow.getSelection(); - range = selection.getRangeAt(0); - node = range.startContainer.childNodes[range.startOffset] ; - - if (range.startContainer === range.endContainer) { - if (range.startContainer.parentNode.nodeName === 'A' && range.commonAncestorContainer !== node) { - currentHyperlink = range.startContainer.parentNode; - } - else if(range.startContainer.parentNode.nodeName === 'A' && SC.browser.safari){ //question for mo here... - currentHyperlink = range.startContainer.parentNode; - } - else { - currentHyperlink = null; - } - - } else { - currentHyperlink = null; - + if (range.parentElement) node = range.parentElement(); + currentHyperlink = this._findAncestor(node, 'A'); + + } else { + // TODO [JS]: remove all logging statements when i'm finally sure it is all working right + /* + SC.Logger.dir(range.startContainer); + SC.Logger.log(range.startOffset); + SC.Logger.dir(range.endContainer); + SC.Logger.log(range.endOffset); + */ + node = range.startContainer.childNodes[range.startOffset]; + if (!node && (range.startContainer === range.endContainer)) { + node = range.startContainer; + } + if (!node && (range.startContainer.nextSibling === this._findAncestor(range.endContainer, 'A'))) { + node = range.startContainer.nextSibling; } } - + if (node) { - if (node.nodeName === 'IMG') { - currentImage = node; - - if(node.parentNode.nodeName === 'A') currentHyperlink = node.parentNode; - - } else if (node.nodeName === 'A') { - currentHyperlink = node; - - } else { - currentImage = null; - currentHyperlink = null; - + //SC.Logger.log("node " + node); + currentImage = node.nodeName === 'IMG' ? node: null; + currentHyperlink = node.nodeName === 'A' ? node : this._findAncestor(node, 'A'); + + // immediately after a selection & link of an IMG, the A tag becomes the node so we have to dig to find the IMG + if (currentHyperlink && currentHyperlink.childNodes.length === 1) { + currentImage = currentHyperlink.firstChild.nodeName === 'IMG' ? currentHyperlink.firstChild : null; } + } else { + //SC.Logger.log("commonAncestor " + range.commonAncestorContainer); + currentHyperlink = range.commonAncestorContainer.nodeName === 'A' ? range.commonAncestorContainer : this._findAncestor(range.commonAncestorContainer, 'A'); } - if (currentHyperlink === null) this.removeLink(); + try { + currentText = selection.toString(); + } catch (e) { + SC.Logger.dir(e); + } + //SC.Logger.log(currentImage); + //SC.Logger.log(currentHyperlink); + //SC.Logger.log(currentText); + + this.set('selectedImage', currentImage); + this.set('selectedHyperlink', currentHyperlink); + this.set('selectedText', currentText); }.observes('selection'), isEditingDidChange: function() { - if (this.get('autoCommit')) { - this.commitEditing(); - } + if (this.get('autoCommit')) { + this.commitEditing(); + } }.observes('isEditing'), - + /** @private */ _updateAttachedViewLayout: function() { var width = this.get('offsetWidth'); @@ -1630,15 +1741,18 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, var view = this.get('attachedView'); var layout = view.get('layout'); - layout = SC.merge(layout, { width: width, height: height }); + layout = SC.merge(layout, { + width: width, + height: height + }); view.adjust(layout); }, - + /** @private */ _updateLayout: function() { var doc = this._document; if (!doc) return; - + var width, height; if (SC.browser.msie) { width = doc.body.scrollWidth; @@ -1662,14 +1776,17 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, if (!this.get('hasFixedDimensions')) { var layout = this.get('layout'); - layout = SC.merge(layout, { width: width, height: height }); + layout = SC.merge(layout, { + width: width, + height: height + }); this.propertyWillChange('layout'); this.adjust(layout); this.propertyDidChange('layout'); } }, - + /** @private */ _getFrame: function() { var frame; @@ -1678,29 +1795,32 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } else { frame = this.$('iframe').firstObject(); } - + if (!SC.none(frame)) return frame; return null; }, - + /** @private */ _getDocument: function() { var frame = this._getFrame(); if (SC.none(frame)) return null; - + var editor; if (SC.browser.msie) { editor = frame.document; } else { editor = frame.contentDocument; } - + if (SC.none(editor)) return null; return editor; }, - + /** @private */ _getSelection: function() { + var frame = this._getFrame(); + if (SC.none(frame)) return null; + var selection; if (SC.browser.msie) { selection = this._getDocument().selection; @@ -1709,44 +1829,41 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } return selection; }, - + _encodeValues: function(html) { var hrefs = html.match(/href=".*?"/gi); if (hrefs) { var href, decodedHref; - - for (var i = 0, j = hrefs.length; i < j; i++) { + + for (var i = 0, + j = hrefs.length; i < j; i++) { href = decodedHref = hrefs[i]; - + html = html.replace(/\%3C/gi, '<'); html = html.replace(/\%3E/gi, '>'); html = html.replace(/\%20/g, ' '); html = html.replace(/\&/gi, '&'); html = html.replace(/\%27/g, "'"); - + html = html.replace(href, decodedHref); } } return html; }, - + _getSelectedElement: function() { - var sel = this._getSelection(), range, elm; + var sel = this.get('selection'), + range = this.get('selectionRange'), + elm; var doc = this._document; - - if (SC.browser.msie) { - range = doc.selection.createRange(); - if (range) { + + if (range) { + if (SC.browser.msie) { elm = range.item ? range.item(0) : range.parentElement(); - } - } else { - if (sel.rangeCount > 0) { - range = sel.getRangeAt(0); - } - - if (range) { + } else { if (sel.anchorNode && (sel.anchorNode.nodeType === 3)) { - if (sel.anchorNode.parentNode) { //next check parentNode + if (sel.anchorNode.parentNode) { + //next check parentNode elm = sel.anchorNode.parentNode; } if (sel.anchorNode.nextSibling !== sel.focusNode.nextSibling) { @@ -1767,21 +1884,20 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } } } - } + } + return elm; } - - return elm; }, - + _resetColorCache: function() { this._last_font_color_cache = null; this._last_background_color_cache = null; this.set('selectionSaved', NO); }, - + saveSelection: function() { this.set('selectionSaved', YES); - + if (SC.browser.msie) { var win = this._getFrame().window; var doc = win.document; @@ -1813,7 +1929,7 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, restoreSelection: function() { this.set('selectionSaved', NO); - + if (SC.browser.msie) { var win = this._getFrame().window; var doc = win.document; @@ -1830,12 +1946,11 @@ SCUI.ContentEditableView = SC.WebView.extend(SC.Editable, } } }, - + convertBgrToHex: function(value) { value = ((value & 0x0000ff) << 16) | (value & 0x00ff00) | ((value & 0xff0000) >>> 16); value = value.toString(16); return "#000000".slice(0, 7 - value.length) + value; } - -}); +});