richtexteditarea.lzx

<!---
      @topic Incubator
      @subtopic Rich Text Editor
  -->
<library>
    <include href="scrollrichedittext.lzx"/>
        
    <!--- 
    A view which contains the text area for a rich text editor. Combine with a 
    richtexttoolbar to make an actual editor. 
    -->
    <class name="richtexteditarea" extends="scrollrichedittext">

        <!--- The placement for the toolbar in the richtexteditor. -->
        <attribute name="toolbarplacement" value="null"/>

        <!--- Reference to the toolbar associated with this editor. 
            For the toolbar to show the correct format when the insertion
            point moves, this attribute must be set by users. -->
        <attribute name="toolbar" value="null"/>
        
        <!--- Reference to the hyperlink creation dialog.
            This attribute should be set by users, if there is 
            an associated link panel --> 
        <attribute name="linkpanel" value="null"/>
        
        <!-- The defaultfontsize, defaultfontname, and defaultfontcolor
             determine the initial configuration of the toolbar, and thus, the
             default font of the editor. --> 

        <!--- Default size of text -->
        <attribute name="defaultfontsize" type="number" value="11"/>
        
        <!--- Default font name -->
        <attribute name="defaultfontname" type="string" value="Verdana"/>
        
        <!--- Default font color -->
        <attribute name="defaultfontcolor" value="0x000000"/> 

        <!--- Indicates whether there's formatting on the text contained by this
              editor. -->
        <attribute name="isHTML" value="false"/> 
        
        <!--- Attribute indicating the type of HTML content produced by editor. -->
        <attribute name="editortype" type="string" value="flash-html"/> 

        <!--- The current format in which text should be added, and which the
              toolbar should reflect.  Everyone interested in the current format
              should look at this attribute to see what it is. -->
        <attribute name="currentformat" setter="setCurrentFormat(currentformat)"/>
        
        <attribute name="startformat"/>

        <!--- The current index of the insertion point. Should be -1 if there's
              no insertion point. -->
        <attribute name="insertionpoint" value="0"/>

        <!--- Start in selection region. 
              @access private -->
        <attribute name="_ssel" value="0"/>

        <!--- End in selectionregion.
              @access private -->
        <attribute name="_esel" value="0"/>
        
        <!--- Whether to inform toolbar of changes in the caret position. 
            @access private -->
        <attribute name="_shouldresettoolbar" value="true"/>

        <!--- The size in points of an indent -->
        <attribute name="indentsize" value="35"/>
                   
        <!--- the component on which a mousedown originated, if the mouse is
            currently down 
            @access private
            --> 
        <attribute name="_mousedownorigin" value="null"/>
        
        <!--- True if the input text associated with this RTE has focus. 
            @access private
            -->
        <attribute name="_fieldhasfocus" value="false"/>

        <!--- True if the next onfocus should be ignored.
              @access private -->
        <attribute name="_ignorenextfocus" value="false"/>
        
        <event name="oncurrentformat"/>

<!-- CONSTRUCTOR    .........................................................-->

        <!--- @keywords private -->
        <method name="init">
            super.init();
            // Create a currentformat object, and initialize it to the format
            // specified by the various defaultfontsize/defaultfontcolor/defaultfontname
            // attributes. 
            this.currentformat = new lz.textformat();
            this._resetFormatToDefaults();
            this.startformat = this.currentformat;
            this.inp.setDefaultFormat(this.currentformat);
        </method>

<!-- PUBLIC METHODS .........................................................-->
        
        <!--- Reset the values of the richtexteditor. -->
        <method name="reset">
            this.clearText(true); 
            this._resetFormatToDefaults();
            this.inp.setDefaultFormat(this.currentformat);
            this.rollback();
            
            if (this.linkpanel) this.linkpanel.hide();
            if (this.toolbar) this.toolbar.reset();
            
            this.isHTML = false;
            this._shouldresettoolbar = true;
            this._ssel = this._esel = 0;
        </method>

        <!--- Hook this editor up to a toolbar, so that the toolbar's ui
            will stay synchronized with the format at the insertion point
            of this editor. --> 
        <method name="setToolbar" args="tb">            
            this.toolbar = tb;
            if (this.isinited) this.toolbar.matchformat(this.currentformat);
        </method>
        
        <!--- Hook this editor up to a link panel, so that the
            linkpanel will be hidden when the editor is reset -->        
        <method name="setLinkPanel" args="lp">
            this.linkpanel = lp;
        </method>


<!-- EVENT HANDLERS .........................................................-->
       
        <!--- When a cursor movement key is released, handle the caret motion.
            @param number kc: keycode
            -->
        <handler name="onkeyup" args="kc" reference="this._field">
            if (isHTML) {
                switch (kc) {
                    case 8:  // backspace key
                    case 46: // delete key
                        if (this.text.length == 0) {
                            this.reset();
            }
                    case 35: // home
                    case 36: // end
                    case 37: // left arrow
                    case 38: // up arrow
                    case 39: // right arrow
                    case 40: // down arrow
                        this._caretmove(); 
                        break;
                }
            }
        </handler>
        

        <!--- When the editor receives focus, we need to make both formats match: 
                 the format of the selection/insertion point and the format displayed in the toolbar.
             
                 How do we choose what to do?
                 If there's no selection, 
                    make the insertion point match the current toolbar state. 
                 else (there is a selection)
                    Make the toolbar match the format of the selected text
        -->
        <handler name="onfocus">
            this._fieldhasfocus = true; 
            if (this._containsOnlyWhitespace()) {
                this.clearText(true); 
                this._ssel = this._esel = 0; 
                this.setSelection(0,0);
                this.inp.setDefaultFormat(this.currentformat); 
            }
            
            // For when setSelection is called explicitly, for example in the
            // linkdialog.
            if (this._ignorenextfocus) {
                this._ignorenextfocus = false;
                return;
            }

            if (this._shouldresettoolbar) {
                if (! this['_caretMoveDel']) {
                    this._caretMoveDel = new lz.Delegate(this, '_caretmove');
                }
                //------------------------------------------------------------
                // Let the caretmove method figure out how to make the formats
                // match. Need to wait an idle frame before calling 
                // _caretmove() because caret focus hasn't been set in 
                // richinput area yet.
                //------------------------------------------------------------
                lz.Idle.callOnIdle(this._caretMoveDel);
                
                if ((! this.isHTML) && this._field.getSelectionSize() == -1)
                {
                    // Selection length of -1 means this field doesn't have focus.
                    // _caretmove() handles this for HTML text only, not plain text
                    delegateInitSelection();
                }
            }
        </handler>
        
        <!--- If the mouse down is over me or any of my children, remember that,
            so we can listen for an onmouseup anywhere. A mouseup after a mousedown
            indicates that the selection has changed.
            @param view who: the element being moused down on
        -->
        <handler name="onmousedown" reference="lz.GlobalMouse" args="who">
            if ((who != null) && (this._containsView(who))) {
                // the mouse went down on a child of me
                // Debug.info("mousedown on child of me, this is %w, who is %#w", this, who); 
                this._mousedownorigin = who; 
            } else if ((who == null) && (inp.containsPt(this.getMouse("x"), this.getMouse("y")))) {
                // the mouse went down on me
                // Debug.write("mousedown on me"); 
                this._mousedownorigin = this; 
            } else {
                // the mouse went down on someone other than me, 
                // Debug.info("mousedown on someone else: %w", who); 
                this._mousedownorigin = null;
            }
            
        </handler>
        

        <!--- Listen for global mouse up. 
            We're only interested in global mouse up if the editor has focus. 
            If the editor does have focus, 
            is there a mousedown/mouseup pair in progress? 
            @param view who: the current mousedown element (ben shine)
        -->
       <handler name="onmouseup" reference="lz.GlobalMouse" args="who">
            if (this._fieldhasfocus) {
                // Debug.write("onmouseup, field has focus, who is ", who); 

                if (this._mousedownorigin != null) {
                    // We have focus, and there _is_ a mousedown/mouseup pair in progress. 
                    if (this._mousedownorigin == this || this._mousedownorigin == this.inp) {
                    
                        // The mouse click started on the richtext editor.
                        
                        // If the mouse click also ended on the rich text editor, 
                        // it's just a caret move. 
                        if (inp.containsPt(this.getMouse("x"), this.getMouse("y"))) {
                            // Debug.info("mouse click starts and ends on RTE"); 
                            this._caretmove(); 
                        } else {  
                            // Debug.write("handleLostMouseDown"); 
                            // If the mouseclick ended anywhere but the rich text editor,
                            // it's a lost mousedown. 
                            this._handleLostMouseDown();                       
                        }
                    } else {
                        // We have focus, and there's a mousedown/mouseup pair in progress,
                        // and it started on one of the subviews of the RTE. 
                    } 

                } else {
                    // We have focus, but there's no mousedown/mouseup pair in progress.
                    // Therefore, do nothing.                     
                    // Debug.write("No mousedown origin. The mousedown didn't start on an RTE component."); 
                }
            }

            // Clear the mousedown attribute; a mouseup means that whatever
            // the mousedown was, it's gone now. 
            this._mousedownorigin = null;          
        </handler>
        
        
        <!--- Save selected region. -->
        <method name="saveSelectionRegion">  
            if (lz.Focus.getFocus() == this._field) {                
                var selpos  = this._field.getSelectionPosition();
                var selsize = this._field.getSelectionSize();
                if( selpos >= 0 && selsize >= 0){  
                    this.setAttribute('_ssel', selpos);
                    this.setAttribute('_esel', selpos + selsize );
                    // Debug.write("fascinating, storing selection of %d-%d", this._ssel, this._esel); 
                }
            } 
            
       </method>
        
        <!--- Change the format of the selection by attributes contained in TextFormat object.
            @param textformat fmt: The TextFormat object to apply to the current selection or the insertion point
            @param boolean inCaretMove: True when the the method was called from a caret motion event
        -->
        
        <method name="setCurrentFormat" args="fmt, inCaretMove">
            // Store the start and end of the selection in temporary local 
            // variables, for performance. 
            // These member data were set in saveSelectionRegion. 
            var ssel = this._ssel;
            var esel = this._esel;
            
            // Record the change to the current format
            this.currentformat = fmt; 
            
            // Apply the new current format to the selection, *unless*
            // this method was called from a caret motion event. We never
            // want to format text in response to a caret motion event. 
            if (!inCaretMove) {
                // We've now applied formatting, so we've made the text have some
                // HTML-ness. 
                this.isHTML = true;
                // if no region is selected, apply format at insertion point
                if (ssel == esel) {             
                    this._formatAtInsertion(ssel);

                } else {
                // there is a region selected, so apply this format to it. 
                    this.setTextFormat(this.currentformat, ssel, esel);
                }
            }
        
            // Inform listeners that the format has changed. 
            this.oncurrentformat.sendEvent(this.currentformat);
            
       </method>
       
    
        <!--- Change the format of the selection by just one attribute. This
            allows the selection to keep non-uniform formats on attributes which
            are not being adjusted. That is: select "pear, cherry", where pear is green
            and cherry is red. Change the font to helvetica using the 
            combo box. Now it's all helvetica, but pear is still green and 
            cherry is still red.
            If no text is currently selected, format the insertion point. 
            This handles character formatting only. Not paragraph formatting.
            See setParagraphAttribute. 
            
            @param string attr: name of the attribute to change
            @param string val: value to change the attribute to
        -->
        <method name="setFormatAttribute" args="attr, val">
            var ssel = this._ssel;
            var esel = this._esel;

            // Debug.write("setFormatAttribute, sel is %d to %d", ssel, esel);             
            if (this['currentformat'])
                this.currentformat[attr] = val;
            this.isHTML = true;
                                    
            // if no region is selected, apply format at insertion point
            if (ssel == esel) {             
                this._formatAtInsertion(ssel);
            } else {
                // there is a region selected, so apply this format to it. 
                var tinyformat = new Object();
                tinyformat[attr] = val; 
                this.setTextFormat(tinyformat, ssel, esel); 
            }                       
       </method>
       
        <!--- Restore the selection region, but do it in the next "frame." This is
            necessary because Flash is frame-based and can't handle certain selection
            changes in a single "frame." 
        -->
        <method name="delegateRestoreSelection">
            // Delegate to restore selection after an edit or focus is lost.
            if (! this['_selectiondel']) {
                this._selectiondel = new lz.Delegate(this, '_restoreSelection');
            }            
            // After the formatting has been completed, need to wait a frame to
            // restore the selection.
            lz.Idle.callOnIdle(this._selectiondel);       
        </method>
        
        <!--- Initialize the selection region on focus event -->
        <method name="delegateInitSelection">
            // Let's send an idle event to call _focussel() in a minute; we'll have focus then
            if (! this['_focusseldel']) {
                this._focusseldel = new lz.Delegate(this, '_focussel');
            }
            lz.Idle.callOnIdle(this._focusseldel);  
        </method>
        
        <method name="setSelection" args="ns,ne=null">
            this._ssel = ns;
            this._esel = ne;
            super.setSelection(ns,ne);
        </method>

<!-- PRIVATE METHODS ........................................................-->

        <!--- Called whenever the caret (aka insertion point) moves in this text field. 
                    // see [BUG ZIM-1268]
              @access private
        --> 
        <method name="_caretmove" args="ignore=null">
        // Don't do any of this if this field isn't already rich-text! 
        if (!this.isHTML) return; 

        //------------------------------------------------------------
        // Analyze the selection position, location, and text length. 
        // 
        // We should never change the format of the selected text 
        // in response to a caretmove event. 
        // 
        // Note: when we tab into this field, the entire field contents are 
        // selected, but we're given selpos of -1, sellen of -1. 
        // Note: we don't get caretmove called when the selection changes by
        // mouseout then mouseup not over richinputtext. 
        //------------------------------------------------------------
        
        var selstart = this._field.getSelectionPosition();
        var sellen = this._field.getSelectionSize();
        var text = this.text;
        var textlen = text.length; 

        // If there's some text here, not just an empty field... 
        if (textlen > 0) {    
            if (sellen == 0) {             // zero-length selection, which means, just an insertion point. 
                // Where are we in the field? 
                if (selstart == 0) {
                    // we're in the beginning of the field. 
                    // get the format of the first character in the field
                    this.setCurrentFormat( this.getTextFormat(0, 1), true);                     
                } else if (selstart == textlen) {
                    // Insertion point is at the end of the current text, so, 
                    // set the format at the insertion point to be the format 
                    // of the toolbar. 
                    
                    //-------------------------------------------------------------
                    // These next two lines are a workaround for a flash bug. 
                    // -ben/pkang
                    //-------------------------------------------------------------
                    this.setSelection(textlen-1, textlen-1);
                    this.setSelection(textlen, textlen);
                    this._ssel = this._esel = textlen; 
                    this.setCurrentFormat( this.getTextFormat(textlen-1, textlen));                  
                } else {
                    // we're in the middle or at the end of the field
                    // get the format of the character immediately before the insertion point
                    this.setCurrentFormat( this.getTextFormat(selstart, selstart + 1), true); 
                }
            } else if (sellen == -1) {
                // Selection length of -1 means this field doesn't have focus.
                // Let's send an idle event to call us in a minute; we'll have focus then
                delegateInitSelection();
            } else {
                // selection length is greater than zero, that is, we have some characters selected

                // Desired behavior:                 
                // Get format of the entire selection.
                var theformat = this.getTextFormat(selstart, selstart + sellen);
                // Update the toolbar with whatever format attributes
                // are common to the entire selection.
                // Show - - (n/a) for any format attributes which are different
                // within the selection. 
                this.setCurrentFormat(theformat, true);
                // This will leave some of the characteristics of the currentformat undefined. 
                
            }
        } else {
            // Debug.write("zero-length field; formatting it."); 
            this._ssel = this._esel = 0; 
            // Set the format of the text we're going to add to be the format currently
            // specified by currentformat! 
            this._formatAtInsertion(0); 
            this.setSelection(0, 0); 
        }

        </method>

        <!--- Called by a delegate after we get focus, to get rid of the default
            select of everything, and instead just move the insertion point to the end
            and make sure the insertion point is formatted properly.
            @access private -->
        <method name="_focussel" args="ignore=null">
            // We really should have focus now! 
            var havelzfocus =  (lz.Focus.getFocus() == this._field); 
            if (havelzfocus) {
                var len = this.text.length;
                // move the insertion point to the beginning of the text
                this.setSelection(len, len);
                this._caretmove(); 
            }
        </method>
        
        <!--- Helper method for applyFormat() to apply format at insertion point.
            Only do this if the message is html already. 
              @param Number ip: insertion point.
              @access private -->
        <method name="_formatAtInsertion" args="ip">
            if (this.isHTML) {
                if (this.text.length == ip){
                    this.inp.setDefaultFormat(this['currentformat']);
                }else{
                    if (this.text.charAt(ip) != ' ') {
                        this.replaceText(ip, ip,' ');    
                    }
                    this.setTextFormat(this['currentformat'], ip, ip+1);
               }    
            }
        </method>

        <!---
            Handle the case where a mouse drag began on the RTE but the mouse up
            was not on the RTE. 
            @access private
        -->
        <method name="_handleLostMouseDown">
            var selstart = this._field.getSelectionPosition();
            var selend = selstart + this._field.getSelectionSize();            
            this.setCurrentFormat( this.getTextFormat(selstart, selend), true);                     
        </method>

        <!--- Restore last selection region. 
            Note: this must be called from a delegate in an idle event
            *after* flash completes the "frame" in which the selection or focus
            changed. 
              @access private -->
        <method name="_restoreSelection" args="ignore=null">
            //------------------------------------------------------------
            // FIXME [2005-08-16 pkang]: Need to know in which direction
            // selection was done so as to not make scrollbar jump.
            //------------------------------------------------------------
            this._shouldresettoolbar = false; 
            this.setSelection(this._ssel, this._esel);
            this._shouldresettoolbar = true; 

        </method>
        
        <!--- Helper method to set an textformat to its default for this
              richtexteditor.
              @param textformat fmt: the format to set to default values.
              @access private -->
        <method name="_resetFormatToDefaults" args="fmt">
            this.currentformat.color = this.defaultfontcolor;
            this.currentformat.font = this.defaultfontname;
            this.currentformat.size = this.defaultfontsize;
            this.currentformat.bold = false; 
            this.currentformat.italic = false;
            this.currentformat.underline = false;
            this.currentformat.bullet = false; 
            this.currentformat.url = "";
        </method>
        
        <!--- Returns whether the passed in node is a child of this view or
            of the toolbar. 
            @param XMLnode? v: ???
            @return string: (need to ask Ben S.)
            @keywords private
        -->
        <method name="_containsView" args="v">
            return (v.searchParents("name", this.name) || v.searchParents("name", this.toolbar.name));
        </method>
        
        <!--- Returns whether the string passed in is entirely whitespace
            @return boolean: True if the textfield is completely whitespace, False if not
            @access private --> 
        <method name="_containsOnlyWhitespace">
            var str = this.text; 
            var len = str.length;
            var ws = " \t\r\n"; 
            for (var i=0; i < len; i++) {
                if (ws.indexOf(str.charAt(i)) < 0) { 
                    return false; 
                }
            }
            return true; 
            
        </method>
    </class>
</library>
<!-- * X_LZ_COPYRIGHT_BEGIN ***************************************************
* Copyright 2005-2011 Laszlo Systems, Inc.  All Rights Reserved.              *
* Use is subject to license terms.                                            *
* X_LZ_COPYRIGHT_END ****************************************************** -->
<!-- @LZX_VERSION@                                                         -->

Cross References

Includes

Classes