All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.smartclient.debug.public.sc.client.widgets.RichTextCanvas.js Maven / Gradle / Ivy

The newest version!
/*
 * Isomorphic SmartClient
 * Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
 * Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
 * "SmartClient" is a trademark of Isomorphic Software, Inc.
 *
 * [email protected]
 *
 * http://smartclient.com/license
 */





  
 

 
//>	@class RichTextCanvas
//  
//  Canvas to be used for Rich Text Editing
//
//<
isc.ClassFactory.defineClass("RichTextCanvas","Canvas");

isc.RichTextCanvas.addClassProperties({
    // enumerated Justification types
    //CENTER:"center",
    //LEFT:"left",
    //RIGHT:"right",
    FULL:"full",

    //>@classAttr   RichTextCanvas.unsupportedErrorMessage  (string : "Rich text editing not supported in this browser" : [IRW])
    // Message to display to the user if they attempt to access the page in a browser which
    // does not support rich-text-editing
    //<
    
    unsupportedErrorMessage : "Rich text editing not supported in this browser"
});

isc.RichTextCanvas.addProperties({

	editable:true,
    
    // Override 'canSelectText': to allow for most editing actions the user must be able to 
    // select the content from this widget.
    canSelectText:true,
    
    // RTC's are focusable
    canFocus : true,

    // Don't write out a focusProxy for RichTextCanvases - we don't want native keyboard 
    // focus to go to a hidden element
    // Instead - in design mode we apply tabIndex directly to the content frame
    // otherwise we rely on native tabIndex to allow focus in the widget handle at
    // the correct times.
    _useFocusProxy:false,
    
    
    overflow:isc.Canvas.AUTO,
    showCustomScrollbars:false,

    // If a syntax rehilite of the entire contents is required, do it this number of
    // milliseconds after the last keystroke (resets to this number every time the user hits a key)
    fullSyntaxHiliteDelay: 3000,
    
    // Don't show a non-breaking space by default
    contents : ""

    // even when hidden the rich text area picks up the bitmap of the area behind it and drags it
    // along as it is scrolled.
    // 
    // XXX not a viable workaround - when hidden in this manner, it fails to show()
//    hideUsingDisplayNone: isc.Browser.isMoz
});

isc.RichTextCanvas.addClassMethods({

    
    //> @classMethod RichTextCanvas.supportsRichTextEditing()    
    //  Does this browser support rich text editing, using the isc.RichTextCanvas class?
    //
    //  @return (boolean)   true if supported by this browser.
    //<
    supportsRichTextEditing : function () {
        var supported = ((isc.Browser.isSafari && isc.Browser.safariVersion >= 312) ||
                         (isc.Browser.isIE) || 
                         // Tested Moz (>=1.4 on Linux / Mac / Windows)
                         //        Firefox (>=1.0 on Linux / Mac / Windows)
                         // Doesn't work on latest camino as of May 17 05 (Version 0.8.4)
                         (isc.Browser.isMoz && !isc.Browser.isCamino) ||
                         isc.Browser.isOpera
                        );
        return supported;
    }
    
});

//!>Deferred
isc.RichTextCanvas.addMethods({

    // On init, verify that we're in a supported browser, and that the overflow is "auto"
    initWidget : function () {
                         
        if (!isc.RichTextCanvas.supportsRichTextEditing()) {
            var errorMessage = isc.RichTextCanvas.unsupportedErrorMessage;
            
            this.logError(errorMessage);
        }
    
        if (this.overflow != isc.Canvas.AUTO) {
            this.logWarn('RichTextCanvas class currently only supports an overflow property of "auto"');
            this.overflow = isc.Canvas.AUTO;
        }
        
        // In "design mode" - where we write out an iframe with editable body content, 
        // turn off native tab index on the handle. We'll instead apply the tabIndex directly
        // to the iframe
        if (this._useDesignMode()) {
            this._useNativeTabIndex = false;
        }
        
        this.Super("initWidget", arguments);
    },
    
    // Override getHandleOverflow - we always have an overflow of "auto" specified on the widget
    // but if we're writing out an editable IFRAME, any scrollbars will show up on the inner 
    // content frame rather than the handle, so we never want to show scrollbars on the handle
    
    _getHandleOverflow : function () {
        if (this._useDesignMode()) {
            var overflow;
            if (this._useMozScrollbarsNone) {
                overflow = "-moz-scrollbars-none";
                this._useMozScrollSize = true;
            } else {
                
                overflow = this._$hidden;
            }
            return overflow;
        } else return this.Super("_getHandleOverflow", arguments);
    },
    
    // getInnerHTML() overridden to write out an editable area.
    getInnerHTML : function () {
    
        // If we're writing out an IFrame with designMode:"On", return the appropriate HTML    
        if (this._useDesignMode() && !this.isPrinting) {
            return this.getIFrameHTML();
        }

        // Otherwise we'll just be setting contentEditable on the standard widget handle.
        //  
        // Note: we used to call Super here, but the Canvas implementation calls getContents()
        // with no args which in this case returns the un-marked-up source, resulting in
        // hilighting breaking on redraw.  In this particular case, return the marked up
        // contents since we'll be assigning to innerHTML
        return this.getContents(true);
    },
    
    // _useDesignMode: Should we achieve our rich text canvas via an IFrame with DesignMode "On",
    // or via a contentEdtiable DIV.
    
    _useDesignMode : function () {
        return isc.Browser.isMoz || isc.Browser.isSafari;
    },

    // ---------- Design Mode / IFRAME handling ------------------
    
    getIFrameHTML : function () {
        
        
        var isSafari = isc.Browser.isSafari,
            URL = isSafari ? isc.Page.getBlankFrameURL() : null,
            
            width = this.getContentFrameWidth() + isc.px,
            height = this.getContentFrameHeight() + isc.px,

        
            srcArray= [
                 
                
                

                ""
        ];
        //this.logWarn(srcArray.join(""));
        
        return srcArray.join(isc.emptyString);
    },
    
    _setHandleTabIndex : function (index) { 
        if (this._useDesignMode()) {
            var frame = this.getContentFrame();
            if (frame != null) frame.tabIndex = index;
        } else {
            return this.Super("_setHandleTabIndex", arguments);
        }
    },
    
    
    // getBrowserSpellCheck - function to determine if we want to use native browser spellcheck
    // functionality where present.
    
    getBrowserSpellCheck : function () {
        return true;
    }, 

    // _frameLoaded - helper method to notify us that the IFRAME has loaded, so we can
    // set up its contents / editability.
    _frameLoaded : function () {
    
        if (!this._drawingFrame) return;
        delete this._drawingFrame;
        if (!this.isDrawn()) return;
        this._setupEditArea();
    },
    
    // Get the ID for the frame in the DOM
    getIFrameID : function () {
        return this.getID() + "_iframe";
    },
    
    // Get a pointer to the IFRAME content document
    getContentDocument : function () {
        
        if (isc.Browser.isIE) return document;

        
        var win = this.getContentWindow(),
            doc = win ? win.document : null;
            
        if (doc == null) {
            // This can happen validly as the document is not always available immediately 
            // after drawing.
            
            this.logDebug("Unable to get pointer to content document. Content may not be written out");
        }
        return doc;
        
    },
    
    // Get a pointer to the document body
    getContentBody : function () {
        var doc = this.getContentDocument();
        if (doc) return doc.body;
        return null;
    },
    
    // Get a pointer to the IFRAME window object.
    getContentWindow : function () {
        var element = this.getContentFrame();
        
        return element ? element.contentWindow : null;
    },
    
    // get a pointer to the IFRAME element in the DOM
    getContentFrame : function () {
        if (!this._useDesignMode() || !this.isDrawn()) return null;

        return isc.Element.get(this.getIFrameID());
    },
    
    
    // Scrolling / Overflow:
    

    // Override setOverflow() to be a no-op. We've already guaranteed the overflow will be
    // 'auto' when the RichTextCanvas is initialized in initWidget().
    setOverflow : function () {
    
    },
    
    // getScrollHandle()    Returns a pointer to the element that gets natively scrolled by
    // calls to scrollTo().
    // - Overridden to point to the content body if _useDesignMode() is true.
    getScrollHandle : function () {
        if (this._useDesignMode()) return this.getContentBody();

        return this.Super("getScrollHandle", arguments);
    },
    
    // Override the internal adjustOverflow method.
    // If we're showing an IFrame, the default implementation will not reliably calculate 
    // whether scrollbars are visible.
    __adjustOverflow : function () {
        // always call the standard 'adjustOverflow' method to ensure we setHandleRect etc as
        // appropriate.
        this.Super("__adjustOverflow", arguments);    
        
        // If we're not writing out an IFrame we can just do normal overflow adjustment
        // Overflows other than "auto" are not really supported - in this case just return too.
        if (!this._useDesignMode() || this.overflow != isc.Canvas.AUTO) return;

        // Update hscrollOn/ vscrollOn - not reliably set by the standard adjustOverflow logic.
        var scrollHeight = this.getScrollHeight(),
            scrollWidth = this.getScrollWidth(),
            height = this.getHeight(), width = this.getWidth(),
            scrollbarSize = this.getScrollbarSize(),
            hscrollOn = false, vscrollOn = false;

        if (scrollHeight > height) vscrollOn = true;
        if (hscrollOn) width -= scrollbarSize;
        if (scrollWidth > width) hscrollOn = true;
        if (hscrollOn && !vscrollOn && (scrollHeight > height - scrollbarSize)) vscrollOn = true;
 
        this.hscrollOn = hscrollOn;
        this.vscrollOn = vscrollOn;
        
    },

    // methods to return the size for the content frame if we're using design mode.
    
    getContentFrameWidth : function () {
       return this.getWidth() - this.getHMarginBorderPad();
    },
    
    getContentFrameHeight : function () {
       return this.getHeight() - this.getHMarginBorderPad();
    },

    // Override _setHandleRect() to always size the IFRAME to match the size of the 
    // handle.
    _setHandleRect : function (left, top, width, height) {
        this.Super("_setHandleRect", arguments);
        
        
        if (this._useDesignMode()) {
            var cf = this.getContentFrame();
            if (cf != null) {
                var innerWidth = this.getContentFrameWidth(), innerHeight = this.getContentFrameHeight();
                cf.style.width = innerWidth + "px";
                cf.style.height = innerHeight + "px";
            }
        }
    },
    
    // Override getScrollHeight() / width to look at the IFRAME body scroll height, since the 
    // IFRAME will always be sized to 100%, making the scroll size of the widget handle always 
    // equal to the specified size.
    getScrollWidth : function (calculateNewValue) {
        if ((this._scrollWidth && !calculateNewValue) || !this._useDesignMode()) 
            return this.Super("getScrollWidth", arguments);
        
        var cb = this.getContentBody();
        if (!cb) return this.Super("getScrollWidth", arguments);

        // cache the scrollWidth for next time this method is called.
        this._scrollWidth = isc.Element.getScrollWidth(cb);
        return this._scrollWidth;
    },
    
    getScrollHeight: function (calculateNewValue) {
        if ((this._scrollHeight && !calculateNewValue) || !this._useDesignMode()) 
            return this.Super("getScrollHeight", arguments);
        
        var cb = this.getContentBody();
        if (!cb) return this.Super("getScrollHeight", arguments);
        
        this._scrollHeight = isc.Element.getScrollHeight(cb);
        return this._scrollHeight;
    },
    
    // --------------------------------------
    
    
    
    
    
    // _rememberSelection() - saves out the current selection position, so we can re-set it
    // when this element regains focus. Only used in IE.
    
    _rememberSelection : function () {
        if (!isc.Browser.isIE) return;

        // Check whether we currently have selection before proceeding - otherwise we could
        // remember some text range outside our handle.
        if (!this._hasSelection()) return;     

        this._savedSelection = document.selection.createRange();
        // Also remember the content of the selection. If the content changes, we don't
        // want a call to '_resetSelection' to select the new text. 
        this._oldSelectionText = this._savedSelection.text;
        // this.logWarn("just saved selection :"+ Log.echo(this._savedSelection));
    },    
    
    // _hasSelection() - Is the current document selection within the RichTextCanvas?
    // Used by _rememberSelection() [IE only]
    _hasSelection : function () {
    
        if (!this.isDrawn()) return false

        if (!isc.Browser.isIE) return;

        
        if (this._useDesignMode()) {
            return (this.getActiveElement() == this.getContentFrame());
        }
        
            
        
        var handle = this.getHandle();
        if (!handle) return false;
        
        var selElement = isc.Element._getElementFromSelection();
        if (!selElement) return false;
        
        return (handle == selElement || handle.contains(selElement));
    },
    
    // Remember the selection every time it changes, so we can reset the selection on focus 
    // or execCommand)
    
    selectionChange : function () {
        if (!this._focussing) this._rememberSelection();
    },
    
    // _resetSelection: resets selection to whatever it was last time this RTC had focus.
    
    _resetSelection : function () {
        if (!this.editable || !this.isDrawn() || !this.isVisible()) return;
        
        if (isc.Browser.isIE) {
            // If no  previous selection, just bail 
            if (!this._savedSelection) return;
            
            // If the content of the range has changed since it was selected, avoid selecting
            // the modified text
            
            if (this._oldSelectionText != this._savedSelection.text) {
                this._savedSelection.collapse(false);
            }
            
            isc.EH._allowTextSelection = true;
            this._savedSelection.select();
            delete isc.EH._allowTextSelection;

        //} else {    //Currently only supported on IE    
        }
        
    },
    
    
    
    // Override setFocus() - when focussing in this widget we want the selection to be
    // whatever it was before the widget was blurred, and for the editable text to have
    // keyboard focus.
    setFocus : function (hasFocus) {
        // Call the Superclass implementation.
        
        this._focussing = true;
        this.Super("setFocus", arguments);
        this._focussing = false;
        
        // If we're using an IFRAME ensure it has focus natively
         
        if (this._useDesignMode()) {
            var win = this.getContentWindow();
            if (!win) return;
            
            if (hasFocus) win.focus()
            
            else window.focus();
        
        // Making this widget's handle contentEditable.
        
        } else {
        
            if (hasFocus) {
            	this._resetSelection();

            }
            
            //else this._rememberSelection();
        }
        
    },
    
    // ------------------- Editor init ------------------
    
    // Override draw to ensure we make the HTML editable when it's done drawing.
    draw : function () {
        this.Super("draw", arguments);
        
        // In Moz / IE, if we're writing out an IFRAME we need to show an event mask
        // for this canvas.
        if (!isc.Browser.isSafari && this._useDesignMode())
            isc.EventHandler.registerMaskableItem(this, true);
        
        // Initialize the contents via _setupEditArea();
        
        if (this._useDesignMode()) {
            this._drawingFrame = true;

        } else {
            this._setupEditArea();
        }

    },
    

    
    redraw : function () {
        var reinitRequired = this._useDesignMode();
        if (reinitRequired) this._rememberContents();
        
        this.Super("redraw", arguments);
        if (reinitRequired) this._drawingFrame = true;
    },

    // _setupEditArea:  Fired when the RichTextCanvas is written into the DOM.
    // This will ensure the appropriate contents and edit state are applied to this widget.
    
    _setupEditArea : function () {
        // Update the HTML to ensure that this is actually editable.

        

        var designMode = this._useDesignMode();
        
        // When using an IFRAME written out in design mode we need to add some custom event
        // handlers.
        if (designMode) {
            // Capture keypresses directly on the IFRAME window so we can fire our
            // keypress handler.
            // Also capture scrolling on the IFRAME directly to update our scroll position,
            // since we're showing native scrollbars on that element.
            
            
            if (!this._editKeyPressHandler) {
                this._editKeyPressHandler = new Function(
                                                 "event", 
                                                 "var returnValue=" + this.getID() + "._iFrameKeyPress(event);" +
                                                 "if(returnValue==false && event.preventDefault)event.preventDefault()"
                                                );
            }
            if (!this._editKeyDownHandler) {
                this._editKeyDownHandler = new Function(
                                                 "event", 
                                                 "var returnValue=" + this.getID() + "._iFrameKeyDown(event);" +
                                                 "if(returnValue==false && event.preventDefault)event.preventDefault()"
                                                );
            }            
            if (!this._editKeyUpHandler) {
                this._editKeyUpHandler = new Function(
                                                 "event", 
                                                 "var returnValue=" + this.getID() + "._iFrameKeyUp(event);" +
                                                 "if(returnValue==false && event.preventDefault)event.preventDefault()"
                                             );
            }                        
            if (!this._editScrollHandler) {
                this._editScrollHandler = new Function(
                                                 "event", 
                                                 "var returnValue=" + this.getID() + "._iFrameScroll(event);" +
                                                 "if(returnValue==false && event.preventDefault)event.preventDefault()"
                                                );
            }
            
            if (!this._editFocusHandler) {
                this._editFocusHandler = new Function(
                                                "event",
                                                this.getID() + "._iFrameOnFocus();"
                                               );
            }
            if (!this._editBlurHandler) {
                this._editBlurHandler = new Function(
                                                "event",
                                                this.getID() + "._iFrameOnBlur();"
                                              );
            }
            var win = this.getContentWindow();
            
            
            
            win.addEventListener("keypress", this._editKeyPressHandler, 
                                                                false);       
            win.addEventListener("keydown", this._editKeyDownHandler, 
                                                                false);       
            win.addEventListener("keyup", this._editKeyUpHandler, 
                                                                false);                                                                                                                                                
            
            win.addEventListener("scroll", this._editScrollHandler, 
                                                                    false);
            win.addEventListener("focus", this._editFocusHandler, false);
            win.addEventListener("blur", this._editBlurHandler, false);
            
            var bodyStyle = this.getContentBody().style;
            // Suppress the default margin
            bodyStyle.margin = "0px";
            
            // Apply text-properties from our specified CSS class to the content of the 
            // IFRAME.
            
            var classStyle = isc.Element.getStyleDeclaration(this.className);
            if (classStyle != null) {
                var textStyleAttrs = isc.Canvas.textStyleAttributes;
                
                for (var i = 0; i < textStyleAttrs.length; i++) {
                    var attr = textStyleAttrs[i];
                    bodyStyle[attr] = classStyle[attr];
                }
            }


        }
        
        // In moz, if we want native spell-check behavior enable it here (otherwise
        // explicitly disable it).
        if (isc.Browser.isMoz) {
            this.getContentBody().spellcheck = (!!this.getBrowserSpellCheck())
        }
        
        var editable = (this.editable && !this.isDisabled());
        
        // Actually make the handle editable
        if (!designMode) this._setHandleEditable(editable);
        else {
            
            this.delayCall("_setHandleEditable", [editable,true], 0);
        }

        // set up our initial contents
        //
        // we're calling _setContents() which means we're bypassing hiliting - this is what we
        // want most of the time because this method is called on redraws when the user may
        // have just resized the browser, so there's no reason to recolorize.  But if this is
        // the first time we're drawing, we need to hilite if we have a syntaxHiliter because a
        // contents may have been provided as an init parameter and if we don't do this it
        // won't get syntax hilited.
        //
        // Note: important to do this after all of the above - to make sure the correcty
        // styling is applied the first time - otherwise we get a partially styled rendering
        // which snaps to the fully styled rendering after about a second - even on fast systems.
        if (this.syntaxHiliter && !this.formattedOnce) {
            this.formattedOnce = true;
            this.contents = this.hiliteAndCount(this.contents);
        }
        this._setContents(this.contents);
    },
    
    // ----------------- Event handling ----------------------
    
    // If using designMode, we need a handler for the native keypress event on our IFRAME
    _iFrameKeyPress : function (event) {
        
        // apply the properties (keyName, etc.) to EH.lastEvent     
        isc.EH.getKeyEventProperties(event); 
        // Fall through to standard handling, making sure this widget is logged as the 
        // keyTarget       
        return isc.EH.handleKeyPress(event, {keyTarget:this});        
    },
    _iFrameKeyDown : function (event) {
        // apply the properties (keyName, etc.) to EH.lastEvent     
        isc.EH.getKeyEventProperties(event); 
        return isc.EH.handleKeyDown(event, {keyTarget:this});        
    },    
    _iFrameKeyUp : function (event) {
        
        // apply the properties (keyName, etc.) to EH.lastEvent     
        isc.EH.getKeyEventProperties(event); 
        return isc.EH.handleKeyUp(event, {keyTarget:this});        
    },    
    
    // If using designMode, we need a handler for the native scroll event on our IFRAME
    // to update the stored scroll position of the handle on scroll.
    // The standard handleCSSScroll method will handle scrolling (as it will check the
    // scroll position of this.getScrollHandle() - which points at the IFRAME).
    _iFrameScroll : function (event) {
        return this._handleCSSScroll(event);
    },
    
    _iFrameOnFocus : function () {
        if (this.destroyed) return;
        isc.EH.focusInCanvas(this, true);
        return true;
    },
    
    _iFrameOnBlur : function () {
        if (this.destroyed) return;
        isc.EH.blurFocusCanvas(this, true);
        return true;
    },
    
    // Adjust overflow on keypress - updates recorded scroll width/height
    _$br:"
", _$Enter:"Enter", // set of keys that are ignored by handleKeyPress because they can't modify the contents of // the editable area. This isn't exhaustive - the main reason to have these is to // eliminate gratuitous syntax hilighting while e.g. the user is using arrow keys to // navigate around the document. ignoreKeys : ["Arrow_Up", "Arrow_Down", "Arrow_Left", "Arrow_Right", "Ctrl", "Alt"], handleKeyPress : function (event, eventInfo) { var key = isc.EH.getKey(); if (this.ignoreKeys.contains(key)) return isc.EH.STOP_BUBBLING; // figure out the start line number of the current selection before the key stroke so // we can extract the modified line(s) later. if (this.countLines) this.rememberSelectionStartLine(); this._queueContentsChanged(); var returnVal = this.Super("handleKeyPress", arguments); // in IE, we set a timer onpaste to do syntax hiliting - this enalbes us to respond to // a paste command initiated via the context menu or "Edit" menu, but when a user hits // a keyboard shortcut to paste, we also get a key press. So if we're responding to a // keypress and there's a paste timer, we delete it so we don't process the paste twice. if (isc.Browser.isIE && this._pasteTimer) { isc.Timer.clearTimeout(this._pasteTimer); delete this._pasteTimer; } if (returnVal != false && isc.Browser.isIE && key == this._$Enter) { this._rememberSelection(); this._savedSelection.pasteHTML(this._$br); this._savedSelection.collapse(true); this._savedSelection.select(); returnVal = false; } return returnVal; }, _queueContentsChanged : function () { if (!this._dirtyContent) { this._dirtyContent = true; if (!this._changedHandlerName) this._changedHandlerName = "_contentsChanged"; isc.Page.setEvent(isc.EH.IDLE, this, isc.Page.FIRE_ONCE, this._changedHandlerName); } }, //_contentsChanged - fired when the contents is edited. // Not fired in response to explicit 'setContents' call. _contentsChanged : function () { delete this._dirtyContent; var oldVal = this.contents, newVal = this.getContents(); if (oldVal == newVal) return; // if we're counting lines, then call doLinesChanged(). We're also checking for // selectionIsCollapsed() here because Ctrl-A, which causes all contents to be selected // should not fire doLinesChanged() - and that's the only known way to get a multichar // selection after a keystroke/paste event if (this.countLines && this.selectionIsCollapsed()) this.doLinesChanged(oldVal, newVal); // AdjustOverflow - our scroll-size is likely to have changed this.adjustOverflow("edited"); // Fire this.changed, if present if (this.changed != null) this.changed(oldVal, newVal); this.contents = newVal; }, // ------------------------------------------------------------------------------------ // lineChanged / Synax Hiliting support // ------------------------------------------------------------------------------------ // // We want to detect any change made to the editable area so we can re-format the view. // Change can be effected in several ways: // - keypress // - paste action using the browser "Edit" menu // - programmatic update (currently only via setContents()) // // At the time of the change, the user may have an insertion cursor or a block of selected // text. // // We want to format the newly added data, and potentially some data around the new data. // To that end we want to: // - mark the location of the current insertion point, so we can restore it after making // changes // - determine the start and end index of the changed text // - expand the above indexes to fully envelop the start and end line of the changed // text. // - we want this because recolorization will happen on a line-by-line basis (for // performance reasons) except for some cases where we'll want to recolorize the // whole document // // In IE and FF we can insert arbitrary HTML at the insertion cursor. This allows us to // get the current location of the cursor. // // General issues //--------------- // - In FF, we can't detect what was pasted unless we compare the original contents // and the new contents - which is expensive. In fact, if the user uses the Edit // menu paste command, then we can't tell the the editable area even changed. There's a // DOM event called "onsubtreemodified" that's part of the w3c spec that should at least // tell us that the contents changed - but it doesn't work at all in current versions of FF // (and this has been corroborated by postings on the web). // - In FF, the contents of the editable area change asynchronously with the key event. In // other words you have to set a timeout to get the after-changed state of the editable area. // - Detecting current selection/insertion point. In IE and FF we can detect the current // selection/insertion point by wrapping the selection contents in a DOM node (e.g. a span) // and then scanning the contents for it. // - In FF we lose the insertion cursor if there's no adjoining text. Also, the cursor // marker that we use to extract the cursor position blocks the user from using the right // arrow on the keyboard to move to the next character beyond it. // // Approaches: // ----------- // 1. Wait for the user to stop typing for a bit and then reformat the entire editable // area. Performance is gated by the formatting algorithm, but this is very easy to // implement and may be acceptable for some use cases. // - benefits // - easy to implement // - problems // - doesn't look as nice, because formatting is not realtime // // 2. Determine the edited line by scanning backwards and forwards from the current // insertion point looking for
s. // - benefits // - easier than maintaining line information in the editable area // - problems // - scanning backwards for
s potentially not cheap // - need to both scan backwards for
(so we can select out the HTML to pass to the // formatter) and walk the DOM to the last
so we can efficiently insert the results. // // 3. Maintain line information in the editable area by inserting line spans into the // contents provided to setContents(). Insert a span with a unique ID into the document at // the current insertion point after a change and walk the // DOM up from that node to find the current line for fast extraction. // - benefits: // - very fast line extraction // - given a selection, can quickly determine what lines it spans. // - after formatting the line, we can replace it effieciently via innerHTML assignment // or equivalent. // - As long as line-based formatting is sufficient for most keystrokes, this approach // gives us an O(1) implementation. // - can get the contents of any line very quickly. Given a line, can get its immediate // surrounding lines quickly. Can get the line number quickly. // - problems // - These spans must be maintained as the user hits Backspace, Enter and pastes // arbitrary content. // - this means fragmenting multiline pastes and those created by user hitting the // Enter key into separate lines and combining lines created by partial line pastes and // Backspace at beginning of line. // - In IE, copying a whole line out of the editable area also picks up its line span // delimiters, which means we can end up with a line inside a line situation. // - I think this can be fixed by defining a custom onbeforepaste handler on the // line spans and filtering the line spans out of the data retrieved from the // clipboard. // - In Moz, the
inside the line span isn't picked up by the copy operation and // is replaced by the paste operation if the paste is at the end of the line. // Further, unlikes IE, the line span isn't placed inside the line that gets pasted // into - instead the pasted-into line is fragmented into two line spans by the // browser and the pasted line is added a peer in between those. // // // line behaviors // -------------- // - type character at beginning of line // - IE, FF: currentLine contains original chars + new char + selection marker +
// - type character in the middle of a line // - IE, FF: as above // - type character at end of a line // - IE, FF: as above // - no special processing required for the above // // - hit enter at beginning of line // - IE, FF: current line contains
origLineText
// - hit enter in the middle of a line // - IE, FF: current line contains origLineTextBeforeEnter
origLineTextAfterEnter
// - hit enter at the end of a line // - IE, FF: current line contains origLineText

// - split current line by
count using regexes to extract new lines // // - hit backspace at beginning of line // - IE: jumps to previous line, deleting the
there. // - FF: previous line missing
, currentLine has at start // - hit delete at end of line // - IE: current line now missing
(basically as IE above) // - FF: as IE // - hit backspace in middle of line // - IE, FF: as typing char in middle of line, except old char deleted // - hit backspace at end of line // - IE, FF: as above // - if current line is missing
, combine with next, otherwise combine previous with current // // - paste external chars (not from a line span) with no BR in line: // - IE: as typing char, but selection goes to front of pasted text // - FF: as typing char // - paste external chars (not from a line span) with BR in line: // - IE, FF: as above, plus a
where there's a linebreak in pasted content // - handled by above cases // // - paste chars from a line span with no BR in line: // - IE: if the copied selection touched the start of the line span, then behaves as FF, // except that the pasted line span appears as a child of the pasted-to line span, not a peer. // Otherwise behaves as paste of external chars // - filter out line spans from pasted content with onbeforepaste, onpaste on line spans, // then handled by above cases as external paste // // - FF: pasted-to line is fragmented into two line spans by the insertion point. pasted // text appears as its own line in between the two. If pasted at end of a line, that // line's BR is moved out of that line's span as the nextSibling the newly created // line span. // - whenever pasting into a line, that line's
is destroyed and either // moves outside the line span (if paste was at end of line) or into a new line // fragment (if there was text to the right of the insertion cursor before paste) // - a paste can be multiline, introducing potentially multiple whole lines, so can't // just look back for a missing
until we hit a normal line (since some intervening // lines may actually be normal, because pasted wholesale from this edit area) // - SOLUTION: // - before paste: // - query total number of lines // - after paste: // - if
is nextSibling outside currentLine (selection always at end of paste), // pull it into currentLine. // - now query total number of lines (after paste). This is the number of lines we // have to walk back looking for missing
s and combining. // // Actual approach used: //---------------------- // #3, except don't combine lines in realtime. Instead, provide a special change() // notification that gives the user the pasted contents and the pasted contents out to line // breaks, and the line number. The user can then format them and call a method to replace // them and insert them at a given location. // // apply a SyntaxHiliter to the contents setSyntaxHiliter : function (syntaxHiliter) { if (syntaxHiliter == null) { this.removeSyntaxHiliter(); return; } this.syntaxHiliter = syntaxHiliter; this.countLines = true; // apply syntax hiliting to the contents var contents = this.getContents() || isc.emptyString; this.setContents(contents); }, removeSyntaxHiliter : function () { // get the contents (do this before we delete this.syntaxHiliter, otherwise the // contents will come with markup. var contents = this.getContents() || isc.emptyString; delete this.syntaxHiliter; delete this.countLines; // apply the contents, now without the markup this.setContents(contents); }, doLinesChanged : function (oldVal, newVal) { // this.logWarn("doLinesChanged - newVal: " + newVal); var startLineNum = this.getLastSelectionStartLine(); // initial setContents() only if (startLineNum == null) return; var startLine = this.getLine(startLineNum); // this.logWarn("startLineNum: " + startLineNum); // if (!startLine) this.logWarn("startLine is null"); // else this.logWarn("startLine: " + startLine.innerHTML); var html = isc.emptyString; var selectionId = this.markCurrentSelection(); if (isc.Browser.isIE) { // startLine contains everything we need - in the event that lines from the editor // got pasted back in, those lines appear as children of the current line and the // line markers will just get stripped out by unescapeHTML() if (!startLine) { this.getLineContainer().innerHTML = isc.emptyString; var line = this.createLine(); this.getLineContainer().appendChild(line); var range = document.selection.createRange(); range.moveToElementText(line); range.collapse(); range.select(); selectionId = this.markCurrentSelection(); startLineNum = 0; startLine = this.getLine(0); } html = startLine.innerHTML; } else { var endLine = this.getSelectionStartLine(); var endLineNum = this.getLineNumber(endLine); if (endLineNum < startLineNum) { startLine = endLine; startLineNum = endLineNum; } // this.logWarn("endLine: " + endLine.innerHTML); // this.logWarn("nextSibling: " + isc.Log.echoAll(endLine.nextSibling)); var currentLine = startLine; var numLines = 0; while (currentLine && currentLine != endLine) { if (currentLine.innerHTML) { html += currentLine.innerHTML; // this.logWarn("html is now: " + html); } numLines++; currentLine = currentLine.nextSibling; } // repair bonus BR that gets shunted out of the original line span by FF var nextNode = endLine.nextSibling; if (nextNode && nextNode.tagName.toLowerCase() == "br") { nextNode.parentNode.removeChild(nextNode); endLine.appendChild(nextNode); // this.logWarn("repaired br, endline is now: " + endLine.innerHTML); } html += endLine.innerHTML; // if we pasted a line span into the middle of another line span, then it will be // fragmented - pick up the trailing fragment if (!html.replace(/\n|\r/g, isc.emptyString).match(/
$/i)) { if (endLine.nextSibling) { html += endLine.nextSibling.innerHTML; numLines++; } } } // this.logWarn("linesChanged: " + html); if (!oldVal) { oldVal = this.contents; newVal = this.getContents(); } // fire linesChanged if it's defined if (this.linesChanged) { this.linesChanged(oldVal, newVal, startLineNum, numLines, html, selectionId); } else if (this.syntaxHiliter) { // currently syntaxHiliter is not compatible with linesChanged - use one or the other. this.doSyntaxHilite(oldVal, newVal, startLineNum, numLines, html, selectionId); } }, doSyntaxHilite : function (oldVal, newVal, startLineNum, numLines, changedHTML, selectionId) { // keeping a marker in the code sent to the colorizer, breaks some colorization cases - // specifically, this happens if the marker is in the middle of something that would be // matched by a regex. For example in XML colorization of the marker is next to the // equal sign in this expression: foo="bar", then that expression won't colorize until // something else is edited such that the selection marker moves. // this.logWarn("source before html removal: " + changedHTML); // remove markup, but keep the selectionSpan so we can extract its index for // repositioning the selection correctly after syntax hiliting var source = this.removeMarkup(changedHTML, true); // this.logWarn("source after html removal: " + source); // save off the index of the locationMarker var selectionMarkerIndex = this.getSelectionMarkerIndex(source); if (selectionMarkerIndex == -1) { // marker has been wiped out by a select-all, re-hilite everything this.doFullSyntaxHilite(); return; } // remove the selectionMarker from the source source = this.removeMarkup(changedHTML); // if the modified source contains a token that requires us to reformat queue a full // hilite, but still do the partial update immediately // if (this.syntaxHiliter.containsMultilineToken(source)) this.queueFullHilite(); // apply the syntax hiliting to just the lines passed in var newLines = this.syntaxHiliter.hilite(source, true, selectionMarkerIndex, this._getSelectionSpanHTML(selectionId)); this.overwriteLines(startLineNum, numLines, newLines); this.moveSelectionToMarker(selectionId); }, doFullSyntaxHilite : function () { // this.logWarn("full syntax hilite running"); var selectionId = this.markCurrentSelection(); var contents = this._getContents(); var source = this.removeMarkup(contents, true); var selectionMarkerIndex = this.getSelectionMarkerIndex(source); if (selectionMarkerIndex == -1) { selectionMarkerIndex = contents.length; } // remove the selectionMarker from the source source = this.removeMarkup(contents); this.setContents(source, true, selectionMarkerIndex, this._getSelectionSpanHTML(selectionId)); this.moveSelectionToMarker(selectionId); delete this.fullHiliteTimer; }, queueFullHilite : function () { // this.logWarn("(re)queueing full hilite"); if (this.fullHiliteTimer) isc.Timer.clearTimeout(this.fullHiliteTimer); this.fullHiliteTimer = this.delayCall("doFullSyntaxHilite", [], this.fullSyntaxHiliteDelay); }, selectionIsCollapsed : function () { if (isc.Browser.isIE) { var range = document.selection.createRange(); return range.text.length == 0; } else if (isc.Browser.isMoz) { var selection = this.getContentWindow().getSelection(); return selection.isCollapsed; } }, rememberSelectionStartLine : function () { this.startLineNum = this.getLineNumber(this.getSelectionStartLine()); }, getLastSelectionStartLine : function () { return this.startLineNum; }, _setPasteTimer : function () { this._pasteTimer = this.delayCall("doLinesChanged", [], 0); }, // onpaste/onbeforepaste for IE - caching theset strings _getOnBeforePaste : function () { if (!this._onBeforePaste) this._onBeforePaste = this.getID()+".rememberSelectionStartLine();event.returnValue=true"; return this._onBeforePaste; }, _getOnPaste : function () { if (!this._onPaste) this._onPaste = this.getID()+"._setPasteTimer();event.returnValue=true" return this._onPaste; }, // line span HTML caching _getLineSpanHTML : function () { if (!this._lineSpanHTML) { this._lineSpanHTML = ""; return line; }, // returns a string containing an incrementing counter usable unique identifier for the // selection span. _getNextSelectionId : function () { if (!this.selectionIdSequence) this.selectionIdSequence = 0; return this.getID()+"_selection_"+this.selectionIdSequence++; }, // returns the DOM node that is the line span containing the start of the current selection. getSelectionStartLine : function () { var doc = this.getContentDocument(); var line; if (isc.Browser.isIE) { var selectionId = this._getNextSelectionId(); var range = doc.selection.createRange(); // collapse the selection range to the start of the current selection range.collapse(); range.pasteHTML(""); var selNode = doc.getElementById(selectionId); line = selNode.parentNode; // this.logWarn("startLine: " + line.outerHTML); line.removeChild(selNode); } else if (isc.Browser.isMoz) { var selection = this.getContentWindow().getSelection(); var line = selection.anchorNode; } // this.logWarn("anchorNode: " + Log.echo(line)); // IE will paste the isLine spans into the current line, so we need to find the // top-most element that is actually a line var lastLine = line; while(line.parentNode != null) { if (line.getAttribute && line.getAttribute("isLine") != null) lastLine = line; line = line.parentNode; } return lastLine; }, _getSelectionSpanHTML : function (selectionId) { return ""; }, // inserts an HTML marker at the start of the current selection. In IE, the marker will be // inserted immediately before the current selection. In FF, immediately after. FF can be // made to work as IE for collapsed selections, but for multichar selections it may be // impossible. See the notes below for enabling IE-style behavior in FF for collapsed // selections. markCurrentSelection : function () { var selectionId = this._getNextSelectionId(); var doc = this.getContentDocument(); if (isc.Browser.isIE) { var range = doc.selection.createRange(); // collapse the selection range to the start of the current selection range.collapse(); // insert our marker span range.pasteHTML(this._getSelectionSpanHTML(selectionId)); } else if (isc.Browser.isMoz) { // create the selection span var selectionNode = doc.createElement("span"); selectionNode.setAttribute('isSelectionSpan', "true"); selectionNode.setAttribute('id', selectionId); // grab the current selection range var selection = this.getContentWindow().getSelection(); var range = selection.getRangeAt(0); if (selection.isCollapsed) { range.insertNode(selectionNode); } else { // create a new range whose start and end match the selection range start boundary var collapsedRange = range.cloneRange(); // collapse the new range to the end of the selection collapsedRange.collapse(false); // insert the selection node at the collapsed range collapsedRange.insertNode(selectionNode); collapsedRange.detach(); /* // The code below will insert the selection marker immediately before the start // of the current selection, but it only works for a collapsed selection. This // is because range.insertNode() puts the new node right after the start of the // selection which means that we need to jump the start of the selection over // the newly inserted node. This works fine for a collapsed selection, but a // multicharacter selection gets destroyed by insertNode() - at least when the // start and end of the multichar selection are in one text node that is // fragmented by insertNode(). After insertNode(), the selection reflects // the character immediately before the previous start of the selection! // Weird FF bug - we need to clear the selection range and then re-add it after // we're done manipulating our clone selection - because if we don't it expands // to the parentNode for no apparent reason. selection.removeAllRanges(); // create a new range whose start and end match the selection range start boundary var collapsedRange = doc.createRange(); collapsedRange.setStart(range.startContainer, range.startOffset); collapsedRange.setEnd(range.startContainer, range.startOffset); // insert the selection node at the collapsed range collapsedRange.insertNode(selectionNode); collapsedRange.detach(); // the above should have been all, but unfortunately the above insertion happened // AFTER the start of the current selection range, so now we need to move the // current selection range start to after the node we inserted if(range.startContainer.nodeType == 3) { // if it's a text node, then the above insertion fragmented the text node into // a two, so just jump over the selectionNode we just inserted. range.setEnd(range.startContainer.nextSibling.nextSibling, 0); range.setStart(range.startContainer.nextSibling.nextSibling, 0); } else { range.setEndAfter(selectionNode); range.setStartAfter(selectionNode); } // add the modified range back so the cursor shows up. selection.addRange(range); */ } } return selectionId; }, overwriteLines : function (lineNum, numLines, newLines) { if (!isc.isAn.Array(newLines)) newLines = [newLines]; var line = this.getLine(lineNum); while (lineNum >= 0 && (!line || !line.getAttribute || !line.getAttribute("isLine"))) { line = this.getLine(lineNum); lineNum-- } if (lineNum < 0) { // this.logWarn("wiping lineContainer"); this.getLineContainer().innerHTML = isc.emptyString; line = this.createLine(); this.getLineContainer().appendChild(line); if (isc.Browser.isMoz) lineNum++; } var container = line.parentNode; // this.logWarn("looking for lineNum: " + lineNum + " - got: " + isc.Log.echoAll(line)); line.innerHTML = newLines[0]; // this.logWarn("replaced line: " + lineNum + " with: " + newLines[0]); // remove the numLines to replace // this.logWarn("removing: " + numLines + " lines"); while (numLines != null && numLines-- > 0) { var removeLine = this.getLine(lineNum+1); if (removeLine) { // this.logWarn("removing line: " + removeLine.innerHTML); // this.logWarn("next line is: " + (removeLine.nextSibling ? removeLine.nextSibling.innerHTML:"null")); container.removeChild(removeLine); } } // add new lines for (var i = 1; i < newLines.length; i++) { if (newLines[i] != -1) this.addLineAfter(lineNum+i-1, newLines[i]); } }, addLineAfter : function (lineNum, line) { // this.logWarn("addAfter: " + lineNum + " line: " + line); var afterLine = this.getLine(lineNum); var nextLine = this.getNextLine(afterLine); line = this.createLine(line); if (nextLine) { nextLine.parentNode.insertBefore(line, nextLine); } else { afterLine.parentNode.appendChild(line); } }, escapeSelection : function (str, escapeValue) { if (escapeValue == null) escapeValue = isc.emptyString; return str.replace(/]*isSelectionSpan[^>]*><\/span>/gi, escapeValue); // var r = new RegExp("]*id=\"?"+selectionId+"[^>]*><\/span>", "gi"); // return str.replace(r, escapeValue); }, getSelectionMarkerIndex : function (s) { var regex = new RegExp("]*isSelectionSpan[^>]*>", "i"); var result = regex.exec(s); if (result) return result.index; return -1; }, getLineNumber : function (line) { var peers = line.parentNode.childNodes; for (var i = 0; i < peers.length; i++) if (peers[i] == line) return i; }, getPreviousLine : function (line) { return line.previousSibling; }, getNextLine : function (line) { return line.nextSibling; }, getLineContainer : function () { return isc.Browser.isIE ? this.getHandle() : this.getContentBody(); }, getLine : function (lineNum) { return this.getLineContainer().childNodes[lineNum]; }, getLineHTML : function (line) { return line.innerHTML; }, getLineContents : function (line) { return this.removeMarkup(this.getLineHTML(line)); }, removeMarkup : function (str, preserveSelectionSpan) { // FF actually inserts \n or \r in addition to
on the ENTER key, so first remove // any literal newlines. // // IE actually inserts tags into the contents in some cases - // it appears to be tracking the currently applied style, so need to remove that HTML // markup here. if (preserveSelectionSpan) { // remove all tag except
and the selectionSpan str = str.replace(/\n|\r|(<\/?(?!br|BR|([^>]*isSelectionSpan)).*?>)/gi, isc.emptyString); } else { // remove all tag except
str = str.replace(/\n|\r|(<\/?(?!br|BR).*?>)/gi, isc.emptyString); } str = str.unescapeHTML(); if (isc.Browser.isOpera) { var nbsp = new RegExp(String.fromCharCode(160), "g"); str = str.replace(nbsp, " "); } return str; }, // given the selectionId of a selection span in the contents, move the selection cursor to // that span. After selection is moved to the span, the selectionMarker is destroyed. moveSelectionToMarker : function (selectionId) { var doc = this.getContentDocument(); var selectionNode = doc.getElementById(selectionId); if (isc.Browser.isIE) { var range = doc.selection.createRange(); range.moveToElementText(selectionNode); range.collapse(); range.select(); } else if (isc.Browser.isMoz) { var selection = this.getContentWindow().getSelection(); selection.removeAllRanges(); var range = doc.createRange(); range.setStartBefore(selectionNode); range.setEndBefore(selectionNode); selection.addRange(range); } this.destroySelectionMarker(selectionId); }, // destroyes the selectionMarker in the contents destroySelectionMarker : function (selectionId) { var doc = this.getContentDocument(); var selectionNode = doc.getElementById(selectionId); if (selectionNode) selectionNode.parentNode.removeChild(selectionNode); }, // ------------ Public Runtime API -------------- //>@method setEditable () // Enable / disable editing of the rich text canvas. // @param editable (boolean) True if we are enabling editing //< setEditable : function (editable) { //this.logWarn("setEditable" + editable); if (editable == this.editable) return; this.editable = editable; this._setHandleEditable(editable); }, // Actually set the handle to be editable or not. _setHandleEditable : function (editable, initialPass) { if (this._useDesignMode()) { var cDoc = this.getContentDocument(); if (cDoc) { if (editable || initialPass) cDoc.designMode = "on"; // Call execCommand directly rather than using our _execCommand method as // we may have 'this.editable' set to false already. if (isc.Browser.isMoz) cDoc.execCommand("readonly", false, editable); if (!editable) cDoc.designMode = "off"; } } else { var handle = this.getHandle(); if (handle != null) { handle.contentEditable = (editable ? true : "inherit"); if (isc.Browser.isIE) { if (!this.isVisible() && this._hasSelection()) this._emptySelectionForHide(); else if (isc.Browser.version < 6) this._rememberSelection(); } } } }, parentVisibilityChanged : function (vis) { if (!this._useDesignMode() && isc.Browser.isIE && (vis == isc.Canvas.HIDDEN) && this._hasSelection()) { this._emptySelectionForHide(); } return this.Super("parentVisibilityChanged", arguments); }, // Helper method for IE to ensure that our selection is empty. _emptySelectionForHide : function () { document.body.focus(); var focusCanvas = isc.EH.getFocusCanvas(); if (focusCanvas != this && focusCanvas != null) { focusCanvas.focus(); } }, // Override disableKeyboardEvents - when disabled we always want to be non-editable disableKeyboardEvents : function (disabled) { this.Super("disableKeyboardEvents", arguments); // If we're editable (when enabled) update the handle to be non editable when // disabled (or make it editable again when enabled) if (this.editable) this._setHandleEditable(disabled ? false : true); }, // ---------- Contents management --------------- // We need APIs for the developer to both set and retrieve the HTML contained in the // editable area. // _rememberContents - stores the contents of the editable area under this.contents. // Note: getContents() should be used rather than checking this.contents directly. _rememberContents : function () { if (!this.isDrawn() || this._drawingFrame) return; var contents = this._getContents(); if (contents != null) this.contents = contents; }, _getContents : function () { var contents; if (this._useDesignMode()) { var c_body = this.getContentBody(); if (!c_body) return; contents = c_body.innerHTML; } else { var handle = this.getHandle(); if (handle) contents = handle.innerHTML; } return contents; }, //> @method RichTextCanvas.getContents() ([]) // Returns current the HTML contents of the RichTextCanvas. // @return (string) (possibly edited) contents // @see RichTextCanvas.setContents() //< getContents : function (dontRemoveMarkup) { this._rememberContents(); // if a syntaxHiliter or line counting is applied, remove line and hiliting information // before returning the contents. if ((this.syntaxHiliter || this.countLines) && !dontRemoveMarkup) { return this.removeMarkup(this.contents); } else { return this.contents; } }, //> @method RichTextCanvas.setContents() ([]) // Changes the contents of a widget to newContents, an HTML string. // @param newContents (string) an HTML string to be set as the contents of this widget // @see RichTextCanvas.getContents() //< setContents : function (contents, force, selectionMarkerIndex, selectionMarkerHTML) { // setContents in effect gets called twice in FF because the iframe takes a while // to load, so an end user calling setContents() directly effectively ends up // setting this.contents which is then picked up via _setupEditArea() if (contents == this.contents && !force) return; // don't hilite if we're not drawn since in that case _setContents() would just return this.contents = contents; if (!this.isDrawn() || this._drawingFrame) return; this._setContents(this.hiliteAndCount(contents, selectionMarkerIndex, selectionMarkerHTML)); }, _setContents : function (contents) { this.contents = contents; if (!this.isDrawn()) return; if (this._useDesignMode()) { var c_body = this.getContentBody(); if (!c_body) return; c_body.innerHTML = contents; } else { var handle = this.getHandle(); if (handle) handle.innerHTML = contents; } // contents have changed, so get updated scrollHeight, check for scrollbars, etc this.adjustOverflow(); }, hiliteAndCount : function (contents, selectionMarkerIndex, selectionMarkerHTML) { if (this.syntaxHiliter) { // if a syntaxHiliter is applied, pass the new contents through it contents = this.syntaxHiliter.hilite(contents, false, selectionMarkerIndex, selectionMarkerHTML); } if (this.countLines) { // if we're initializing an empty richTextEditor and we're going to be doing // realtime hiliting, we must have one of our special lines in there - otherwise // all related logic will break. If the contents is blank, insert a
because // the regex immediately below relies on it and can't be efficiently changed to // capture this case. if (contents == isc.emptyString) contents = "
"; contents = contents.replace(/((?:.*?
)|(?:.+$))/gi, this._getLineSpanHTML()); } return contents; }, // adds the specified contents to the innerHTML appendContents : function (contents, selectionMarkerIndex, selectionMarkerHTML) { contents = this.hiliteAndCount(contents, selectionMarkerIndex, selectionMarkerHTML); var handle = this._useDesignMode() ? this.getContentBody() : this.getHandle(); handle.innerHTML += contents; // contents have changed, so get updated scrollHeight, check for scrollbars, etc this.adjustOverflow(); }, // --------------- Rich Text Editing Commands ------------------- // _execCommand() Fires the standard 'execCommand()' method to modify the editable // content. // We currently use the native 'document.execCommand()' method to perform most of our // actions on the text of the RTC, so most of our Rich Text APIs fall through to this // wrapper method. // Will return explicitly return 'false' if the command is not supported. _execCommand : function (command, valueString) { if (!this.isDrawn() || !this.editable) return; // We could use 'queryCommandEnabled()' here to determine whether the command is valid // given the current selection, etc. if (!isc.Page.isLoaded()) { this.logWarn("Unsupported attempt to manipulate RichTextCanvas content style " + "before page load: postponed until the page has done loading."); isc.Page.setEvent( "Load", this.getID() + "._execCommand('" + command + "','" + valueString + "');" ); return; } // Ensure we have focus and are selected. this.focus(); var designMode = this._useDesignMode(), doc = designMode ? this.getContentDocument() : document; if (!doc) return; // If the command is unsupported, return false to notify callers. if (!this._commandEnabled(command)) return false; try { doc.execCommand(command, false, valueString); } catch (e) { return false; } if (designMode) { // put focus into the window so the user can continue typing and have // an effect. var cw = this.getContentWindow(); cw.focus(); } else { // text manipulation is likely to have changed the text insertion point. this._rememberSelection(); } // Fire _contentsChanged() - it is likely that the execCommand changed the content // of the RTC. this._contentsChanged(); }, // Helper method to test for command being enabled _commandEnabled : function (command) { try { var doc = this._useDesignMode() ? this.getContentDocument() : document; if (!doc) return false; if (!doc.queryCommandEnabled(command)) return false; } catch (e) { return false; } return true; }, //>@method RichTextCanvas.boldSelection // Toggle whether the current text selection is bold or not bold //< boldSelection : function () { this._execCommand("bold") }, //>@method RichTextCanvas.italicSelection // Toggle whether the current text selection is italic or not //< italicSelection : function () { this._execCommand("italic"); }, //>@method RichTextCanvas.underlineSelection // Toggle whether the current text selection is underlined or not //< underlineSelection : function () { this._execCommand("underline"); }, //>@method RichTextCanvas.strikethroughSelection // Toggle whether the current text selection is strikethrough or not //< strikethroughSelection : function () { this._execCommand("strikethrough"); }, //>@method RichTextCanvas.showClipboardDisabledError // For some browsers the clipboard functions (cut/copy/paste) are disabled by default. // We catch these cases from cutSelection() / copySelection() / pasteOverSelection() and // call this method to warn the user. Default behavior shows a warn dialog with the text // "Your browser does not allow web pages to access the clipboard programmatically." // May be overridden to change this behavior. // @visibility editor_clipboard //< // Cut/Copy/Paste all disabled by default in Moz. // In Safari Cut/Copy work, but Paste always fails. // In IE all three methods are enabled and work by default. showClipboardDisabledError : function () { var errorMessage = "Your browser does not allow web pages to access the clipboard programmatically."; // Mozilla allows you to turn on clipboard access for scripts but it's somewhat complex // and you can only turn it on for specific URLs // There's a note on this at mozilla.org: http://www.mozilla.org/editor/midasdemo/securityprefs.html // NOTE: the freeware HTMLArea application available at: // http://www.dynarch.com/projects/htmlarea/ or // http://www.postnukepro.com/modules/pagesetter/guppy/HTMLArea30beta/ // points the user directly to this page. We currently don't because: // - the steps to enable the script access are complex // - the explanation specifically refers to the Moz "midas" demo app // - we don't know of any way to turn on "Paste" access in Safari. isc.warn(errorMessage); }, //>@method RichTextCanvas.copySelection // Copy the current text selection to the clipboard. // @visibility editing_clipboard //< copySelection : function () { if (this._execCommand("copy") == false) this.showClipboardDisabledError(); }, //>@method RichTextCanvas.cutSelection // Copy the current text selection to the clipboard, and remove it from the edit area. // @visibility editing_clipboard //< cutSelection : function () { if (this._execCommand("cut") == false) this.showClipboardDisabledError();; }, //>@method RichTextCanvas.pasteOverSelection // Paste the current clipboard text over the selection // @visibility editing_clipboard //< pasteOverSelection : function () { if (this._execCommand("paste") == false) this.showClipboardDisabledError(); }, //>@method RichTextCanvas.deleteSelection // Delete the currently selected text //< deleteSelection : function () { this._execCommand("delete"); }, //>@method RichTextCanvas.indentSelection // increase the indent for the currently selected paragraph //< indentSelection : function () { this._execCommand("indent"); }, //>@method RichTextCanvas.outdentSelection // decrease the indent for the currently selected paragraph //< outdentSelection : function () { this._execCommand("outdent"); }, //>@method RichTextCanvas.justifySelection // Applies the alignment / justification passed in to the selected paragraph. // Options are "right", "left", "center" (for alignment), and "full" for fully justified // text. // @param justification (string) What justification should be applied to the text. //< justifySelection : function (justification) { if (justification == isc.RichTextCanvas.CENTER) { this._execCommand("justifycenter"); } else if (justification == isc.RichTextCanvas.FULL) { this._execCommand("justifyfull"); } else if (justification == isc.RichTextCanvas.RIGHT) { this._execCommand("justifyright"); } else if (justification == isc.RichTextCanvas.LEFT) { this._execCommand("justifyleft"); /* } else { this._execCommand("justifynone"); */ } }, //>@method RichTextCanvas.setSelectionColor // Set the font color for the selected text. Takes the desired color as a parameter. // @param color (string) Color to apply to the text. //< setSelectionColor : function (color) { this._execCommand("forecolor", color); }, //>@method RichTextCanvas.setSelectionBackgroundColor // Set the background color for the selected text. Takes the desired color as a parameter. // @param color (string) Color to apply to the text background. //< setSelectionBackgroundColor : function (color) { // In Moz "backcolor" will style the entire containing IFRAME - while 'hilitecolor' // will set the background color for just the selected text. var command = isc.Browser.isMoz ? "hilitecolor" : "backcolor"; this._execCommand(command, color); }, //>@method RichTextCanvas.setSelectionFont // Set the font for the selected text. Takes the name of a font as a parameter. // @param font (string) Font to apply to the selection //< setSelectionFont : function (font) { this._execCommand("fontname", font); }, //>@method RichTextCanvas.setSelectionFontSize // Set the size of the font for the selected text. Takes a number between 1 and 7. // @param size (number) Desired font size - a value between 1 and 7. //< setSelectionFontSize : function (size) { this._execCommand("fontsize", size); }, createLink : function (url) { this._execCommand("CreateLink", url); } }); isc.RichTextCanvas.registerStringMethods({ changed : "oldValue,newValue" }); //!




© 2015 - 2024 Weber Informatics LLC | Privacy Policy