root/trunk/common/Editor/Iframe.js

Revision 199, 31.4 kB (checked in by ddavis, 3 years ago)

Make editor changes easier to detect in a subclass. BugzID: 32161

  • Property svn:keywords set to Id
Line 
1/*
2$Id$
3
4Copyright Six Apart, Ltd. All rights reserved.
5Redistribution and use in source and binary forms is
6subject to the Six Apart JavaScript license:
7
8http://code.sixapart.com/svn/js/trunk/LICENSE.txt
9*/
10
11
12Editor.Iframe = new Class( Component, {
13
14    FORMAT_BLOCK_TAG: "div",
15   
16    /**
17     * class: <code>Editor.Iframe</code><br/>
18     * @param element <code>Node</code> The dom node to use for the rich-text editor area.
19     * @param editor <code>Editor</code> The "manager" of this object.
20     */
21    initObject: function( element, editor ) {
22        arguments.callee.applySuper( this, arguments );
23       
24        this.editor = editor;
25        this.window = this.element.contentWindow;
26        this.document = this.element.contentDocument || this.element.contentWindow.document;
27       
28        if( defined( this.document.body.contentEditable ) )
29            this.document.body.contentEditable = true;
30        else
31            this.document.designMode = "on";
32       
33        try {
34            this.document.execCommand( "useCSS", false, true );
35            this.document.execCommand( "styleWithCSS", false, false );
36        } catch( e ) {}
37       
38        // clear out the document completely
39        this.document.body.innerHTML = " ";
40    },
41   
42   
43    destroyObject: function() {
44        this.savedSelection = null;
45        this.document = null;
46        this.window = null;
47        this.editor = null;
48        arguments.callee.applySuper( this, arguments );
49    },
50   
51
52    /* events */
53
54    /**
55     * class: <code>Editor.Iframe</code><br/>
56     */
57    initEventListeners: function() {
58        this.addEventListener( this.document, "mousedown", "eventMouseDown", true );
59        this.addEventListener( this.document, "mouseup", "eventMouseUp", true );
60        this.addEventListener( this.document, "mouseover", "eventMouseOver", false );
61        this.addEventListener( this.document, "mouseout", "eventMouseOut", false );
62       
63        this.addEventListener( this.document, "click", "eventClick", true );
64        this.addEventListener( this.document, "dblclick", "eventDoubleClick", true );
65        this.addEventListener( this.document, "contextmenu", "eventContextMenu" );       
66       
67        this.addEventListener( this.document, "keydown", "eventKeyDown", true );
68        this.addEventListener( this.document, "keypress", "eventKeyPress", true );
69        this.addEventListener( this.document, "keyup", "eventKeyUp", true );
70       
71        this.addEventListener( this.document, "focus", "eventFocus" );
72        this.addEventListener( this.document, "focusin", "eventFocusIn" );
73        this.addEventListener( this.window, "blur", "eventBlur" );
74        this.addEventListener( this.element, "beforedeactivate", "eventBeforeDeactivate" );
75    },
76   
77   
78    /**
79     * class: <code>Editor.Iframe</code><br/>
80     * See the class-level jsdoc on this event.
81     * @param event  <code>Event</code> A prepared event object.
82     */
83    eventBlur: function( event ) {
84        this.saveSelection();
85    },
86   
87
88    /**
89     * class: <code>Editor.Iframe</code><br/>
90     * See class-level jsdoc on this event.
91     * @param event  <code>Event</code> A prepared event object.
92     */
93    eventBeforeDeactivate: function( event ) {
94        if( event.target !== this.element )
95            return;
96        this.saveSelection();
97    },
98
99
100    /**
101     * class: <code>Editor.Iframe</code><br/>  See doc on <code>this.eventKeyPress</code>.
102     * @param event  <code>Event</code> A prepared event object.
103     * @return variant <ul>
104     *                     <li><code>boolean</code> <code>false</code> if keydown was an
105     *                         application-level command (i.e., save-post). </li>
106     *                     <li>. . . or otherwise, <code>undefined</code> if no character deletion was performed</li>
107     *                     <li> . . . or otherwise, <code>variant</code> -- the result of the
108     *                          <code>this.captureDelete</code> method</li>
109     *                 </ul>
110     */
111    eventKeyDown: function( event ) {
112        if( event.ctrlKey || event.metaKey ) { 
113            // Here is a hook to allow the application ('app') responds to its own keybindings.
114            var appBinding = app.eventKeyDown ? app.eventKeyDown( event ) : void 0; 
115            if( defined( appBinding ) )
116                return appBinding;
117        }
118
119        this.monitorSelection();
120        this.editor.setChanged();
121        switch( event.keyCode ) {
122            case 8:     // backspace
123            case 46:    // delete
124                return this.captureDelete( event );
125        }
126    },
127
128
129    /**
130     * class: <code>Editor.Iframe</code><br/>  This method is used for handling
131     * application-specific event-modifier-key commands, such as save-post.  However, the
132     * specific commands themselves are left up to extending classes of <code>Editor</code>. 
133     * The associciated events need to be stopped to prevent them from triggering operating-system specific
134     * actions, such as the save-this-page-as dialog on ('cntrl/cmd-s').                   
135     * @param event  <code>Event</code> A prepared event object.
136     * @return variant <ul>   
137     *                     <li><code>boolean</code> <code>false</code>if a valid Editor command was
138     *                         sent in the event (i.e., bold,
139     *                         italic, etc.) or application-level command (i.e., save-post)
140     *                         exists in the key event command</li>
141     *                     <li> . . . or otherwise, nothing (result <code>undefined</code>)</li>
142     *                 </ul>
143     */
144    eventKeyPress: function( event ) {
145        var command = this.editor.getKeyEventCommand( event );
146        if( command ) {
147            this.execCommand( command, false, null );
148            return event.stop();
149        }       
150    },
151
152
153    /**
154     * class: <code>Editor.Iframe</code><br/>
155     * @param event  <code>Event</code> A prepared event object.
156     */
157    eventKeyUp: function( event ) {
158        this.monitorSelection( event );
159    },
160
161
162    /**
163     * class: <code>Editor.Iframe</code><br/>
164     * @param event  <code>Event</code> A prepared event object.
165     * @return boolean true (always -- for IE 6: Ensure the survival of this event beyond the capturing.)
166     */
167    eventMouseDown: function( event ) {
168        this.monitorSelection( event );
169        return true; 
170    },
171   
172
173    /**
174     * class: <code>Editor.Iframe</code><br/>
175     * @param event  <code>Event</code> A prepared event object.
176     */
177    eventMouseUp: function( event ) {
178        this.eventClick( event );
179    },
180   
181   
182    /**
183     * class: <code>Editor.Iframe</code><br/>
184     * @param event  <code>Event</code> A prepared event object.
185     */
186    eventClick: function( event ) {       
187        this.monitorSelection( event );
188    },
189
190
191    /* misc */
192
193    /**
194     * class: <code>Editor.Iframe</code><br/>
195     * @param event  <code>Event</code> A prepared event object.
196     */
197    focus: function( event ) {
198        if( this.element.focus ) { // try-catches silence incorrect NS error on Mozilla (FF 1.5).
199            try {
200                this.element.focus();
201            } catch ( e ) {};
202        }     
203        if( this.window.focus ) {
204            try { 
205                this.window.focus();
206            } catch ( e ) {};
207        }
208        this.monitorSelection( event );
209    },
210   
211   
212    /* selection */
213
214    /**
215     * class: <code>Editor.Iframe</code><br/>
216     * @return Selection The current selection in the rich-text editor, or the
217     *                   insertion point (collapsed selection).
218     */
219    getSelection: function() {
220        return DOM.getSelection( this.window, this.document );
221    },
222
223
224    /**
225     * class: <code>Editor.Iframe</code><br/>
226     * Used for saving the selection on browsers that lose the current selection when
227     * a screen element outside of the current selection is clicked/selected.
228     */
229    saveSelection: function() {
230        var selection = this.getSelection();
231        if( selection.createRange ) {
232            var range = selection.createRange();
233            if( range.parentElement ) {
234                var element = range.parentElement();
235                if( element && element.ownerDocument === this.document ) 
236                    this.savedSelection = range.getBookmark();
237            }
238        } else if( selection.getRangeAt ) 
239            this.savedSelection = selection.getRangeAt( 0 ).cloneRange();
240    },
241
242
243    /**
244     * class: <code>Editor.Iframe</code><br/>
245     */
246    restoreSelection: function() {
247        this.focus();
248        if( !this.savedSelection )
249            return;
250
251        var selection = this.getSelection();
252
253        if( selection.createRange ) {       
254            var range = this.document.body.createTextRange();       
255            range.moveToBookmark( this.savedSelection );
256            range.select();
257        } else if( selection.getRangeAt ) {
258            selection.removeAllRanges();           
259            selection.addRange( this.savedSelection );                         
260        }   
261       
262        this.savedSelection = null;
263    },
264
265
266    /**
267     * class: <code>Editor.Iframe</code><br/>
268     */
269    deleteSelection: function() {
270        var selection = this.getSelection();
271       
272        // internet explorer
273        if( selection.createRange ) {
274            var range = selection.createRange();
275            range.execCommand( "delete", false, null );
276        }
277
278        // mozilla
279        else {
280            this.document.execCommand( "delete", false, null );
281        }
282    },
283   
284
285    /**
286     * class: <code>Editor.Iframe</code><br/>
287     * Keep the insertion point and selection in sane condition.
288     * @param event  <code>Event</code> A prepared event object.
289     */   
290    monitorSelection: function( event ) {
291        var selection = this.getSelection();
292           
293        // mozilla/w3c:
294        if( selection.rangeCount ) {
295            this.editor.updateToolbar( selection );
296            var range = selection.getRangeAt( 0 ).cloneRange();
297            var collapsed = range.collapsed;
298           
299            if( event && event.type == "mousedown" && event.button != 2  ) { // The 'range' methods don't deselect.
300                selection.collapse( selection.anchorNode, selection.anchorOffset ); // Don't do this on right-click
301                collapsed = true; 
302            }
303            // test for immutable elements
304            var immutable = DOM.getImmutable( range.startContainer );
305            if( immutable ) {
306                range.setStartBefore( immutable );
307                if( collapsed )
308                    range.collapse( true );
309                else
310                    range.setEndAfter( immutable );
311            }
312           
313            // test for mouseup, setting caret at the end of a link
314            else if( collapsed && event && event.type && event.type.match( /keyup|mouseup/ ) ) {
315                var arrowKey = "";
316                if( event.keyCode == 37 )
317                    arrowKey = "left";
318                else if( event.keyCode == 39 )
319                    arrowKey = "right";     
320                this.setCaretOutsideElement( selection, range, arrowKey, event.type.match( /mouseup/ ) );           
321            }
322            // nada
323            else
324                return;
325         
326            // do it
327            selection.removeAllRanges();           
328            selection.addRange( range );
329        } 
330        else { // If the user did not type a printing character, update the toolbar button state:
331            if( event && event.type.match( /keyup|mouseup/ ) ) { // Note: 'jsc' doesn't work with literal w.s. in literal regexps.
332                if( event.keyCode && ( new RegExp( "\\w| " ).test( String.fromCharCode( event.keyCode ) ) ) ) 
333                    return;  // ** FIX - regexp above won't work with non-Latin charsets; check range.text.length, etc. instead.
334                var updateToolbar = this.editor.getIndirectMethod( "updateToolbar" );
335                var callUpdate = function() { updateToolbar( selection ); }; //    IE needs the time; otherwise,
336                new Timer( callUpdate, 400, 1 );       // it sometimes returns the wrong object as the selection.
337            }
338        }
339       
340    },
341   
342
343    /* immutables */
344
345    /**
346     * class: <code>Editor.Iframe</code><br/>
347     * @param node <code>Node</code> Any dom node.   
348     * @return Node If element is not a text node and meets requirements of method
349     *              <code>DOM.isImmutable</code> (<code>null</code> otherwise).
350     */
351    isContentOrImmutable: function( node ) {
352        if( node.nodeType == Node.TEXT_NODE && node.nodeValue.match( /\S/ ) )
353            return null;
354        if( DOM.isImmutable( node ) )
355            return node;
356    },
357
358
359    /**
360     * class: <code>Editor.Iframe</code><br/>
361     * @param node <code>Node</code> Any dom node.   
362     * @return Node (<code>null</code> if none)
363     */
364    getPreviousImmutable: function( node ) {
365        return DOM.Proxy.forPrevious( node, this.isContentOrImmutable );
366    },
367
368
369    /**
370     * class: <code>Editor.Iframe</code><br/>
371     * @param node <code>Node</code> Any dom node.   
372     * @return Node (<code>null</code> if none)
373     */
374    getNextImmutable: function( node ) {
375        return DOM.Proxy.forNext( node, this.isContentOrImmutable );
376    },
377
378
379    /**
380     * class: <code>Editor.Iframe</code><br/>
381     * Perform deletion in a manner pleasing to the user.
382     * @param event  <code>Event</code> A prepared event object.
383     * @return variant <ul>
384     *                     <li><code>undefined</code> if <code>event.ctrlKey</code> or
385     *                     w3c/mozilla selection</li>
386     *                     <li> . . . or otherwise, <code>boolean</code> <code>false</code>.</li>
387     *                 </ul>
388     */
389    captureDelete: function( event ) {
390        var selection = this.getSelection();
391        if( event.ctrlKey )
392            return; // Control deletes/backspaces work fine natively.
393   
394        // internet explorer control selection
395        if( selection.type == "Control" ) 
396            this.deleteSelection();
397       
398        // internet explorer zero-width text selection
399        else if( selection.createRange && selection.type == "None" ) {
400            var range = selection.createRange();
401            var bookmark = range.getBookmark();
402
403            // - - - - Prep range for deletion:
404            if( event.keyCode == 8 || event.keyCode == 46 ) { // backspace is 8; delete is 46.
405                var vector = event.keyCode == 8 ? -1 : 1;
406                var originalText = range.text;
407                range.moveStart( "character", vector ); 
408                // Avoid getting stuck behind a block-level element: If not on text, go into fight-IE-to-the-death mode:
409                if( vector == -1 && !range.text.length ) { 
410                    range.moveStart( "character", vector ); // Pegged!
411                    if( range.text.length ) // Catch-and-release if in a block level element containing text.
412                        range.moveStart( "character", ( -1 ) * vector );
413                    var nonCharDelete = true;
414                }
415            }
416            range.select();
417
418            // - - - - Do deletion:
419            selection = this.getSelection();
420            if( selection.type == "Control" || nonCharDelete ) { // Handle non-characters and hard-to-delete situ's:
421                //                                 .*.
422                //      Nuke!                 '''*((@))*'''       
423                this.deleteSelection(); // .  . ' ._|_. ' .  .   
424                range.select(); // Needed to avoid cursor jumping after deletion.
425            } else { // Easy deletions: Handle deletion/backspace of a character and other situations:
426                if( vector == 1 ) { // IE needs a helping-hand with forward-delete:
427                    range.moveToBookmark( bookmark ); 
428                    range.select();
429                } else if( range.text == originalText ) { // Avoid forward-deleting on backspace at top of post:
430                    range.collapse( true ); // Keeps the selection easy for IE to handle (avoid lock-up).
431                    return false;
432                } // Catch-all for the easy deletions:
433                this.document.execCommand( "delete", false, null ); 
434            }           
435        }
436
437        // mozilla
438        else if( selection.getRangeAt ) {
439            if( !selection.isCollapsed )
440                return;
441
442            var range = selection.getRangeAt( 0 );
443            var node = range.startContainer;
444            var offset = range.startOffset;
445
446            var immutable;
447
448            // backspace
449            if( event.keyCode == 8 && ( node.nodeType != Node.TEXT_NODE || offset == 0 ) ) {
450                immutable = this.getPreviousImmutable( node );
451                try {
452                    if( immutable )
453                        range.setStartBefore( immutable );
454                } catch( e ) {}
455            }
456
457            // delete
458            else if( event.keyCode == 46 && ( node.nodeType != Node.TEXT_NODE || 
459                     offset == node.nodeValue.length ) ) {
460                immutable = this.getNextImmutable( node );
461                try {
462                    if( immutable )
463                        range.setEndAfter( immutable );
464                } catch( e ) {}
465            }
466
467            // set the selection to enclose the immutable
468            if( immutable ) {
469                selection.removeAllRanges();
470                selection.addRange( range );
471                this.deleteSelection(); // let mozilla handle deletion?
472            }
473           
474            return;
475        }
476       
477        // anything else
478        else
479            this.document.execCommand( "delete", false, null );
480       
481        // we own these keys
482        return event.stop();
483    },
484   
485   
486    /* html filtering */
487
488    /**
489     * class: <code>Editor.Iframe</code><br/>
490     * @return string The html "behind" the display in the rich-text editor area.
491     */   
492    getHTML: function() {   
493        var html = this.document.body.innerHTML;
494        return html;
495    },
496
497
498    /**
499     * class: <code>Editor.Iframe</code><br/>
500     * Set all the html of the rich-text editor.
501     * @param html <code>string</code> The html to set.
502     */
503    setHTML: function( html ) {
504        this.formatBlock(); // bugid: 39059
505        this.document.body.innerHTML = html;
506    },
507
508
509    /**
510     * class: <code>Editor.Iframe</code><br/>
511     * @param html <code>string</code> The html to insert at point.
512     * @param select <code>boolean</code> OPTIONAL Whether or not to select the inserted html (works on
513     * text selections only).
514     * @param id <code>string</code> OPTIONAL Only needed and used on IE (6 at this time).
515     *           An i.d. to use to retrieve the inserted element, used if
516     *           element cannot be found via the <code>selection</code> object (as is the case on IE 6
517     *           when pasting in an html element).
518     * @param isTempId <code>boolean</code> OPTIONAL If <code>true</code>, remove the <code>id</code>
519     *                 attribute supplied above after the method is done using it.
520     * @return Node An object that is the inserted element.
521     */
522    insertHTML: function( html, select, id, isTempId ) { 
523        this.beginCommand();
524        this.restoreSelection();
525        var selection = this.getSelection();
526        var inserted = null;
527        if( selection.createRange ) { // Internet Explorer (IE)
528            var range = selection.createRange();
529            if( selection.type == "None" || selection.type == "Text" ) {
530                try {
531                    range.pasteHTML( html );
532                } catch ( err ) {
533                    log( "Error pasting html on selection of type 'Text' or 'None': " + err );               
534                }
535                if( defined( id ) ) {
536                    inserted = this.document.getElementById( id );
537                    if( select ) 
538                        range.moveToElementText( inserted );                       
539                } else {
540                    if( range.moveStart ) {
541                        range.moveStart( "character", ( ( html.length ) * ( -1 ) ) );                           
542                        inserted = range.parentElement();
543                    }
544                }
545                if( select ) 
546                    range.select();
547            } else { // IE 'Control' selection   
548                range.item( 0 ).outerHTML = html; // Not perfect but much better than nothing.               
549                inserted = range.item( 0 ); 
550            }
551        }
552 
553        // mozilla: Try to use w3c "range" methods where possible instead of proprietary "selection" methods.
554        else if( selection.getRangeAt ) {
555            var range;
556            if( selection.rangeCount )
557                range = selection.getRangeAt( 0 );
558            else {
559                range = this.document.createRange();
560                range.setStart( this.document.body, 0 );
561                range.setEnd( this.document.body, 0 );
562                selection.addRange( range );
563            }
564            var anchor = range.startContainer;
565
566            // Enable the user to set caret after inserted element by clicking there:
567            if( selection && range && this.isCaretAtEnd( selection, range ) ) { 
568                var paragraph = this.document.createElement( this.FORMAT_BLOCK_TAG );
569                paragraph.insertBefore( this.document.createElement( "br" ), null );
570                this.document.getElementsByTagName( "body" )[ 0 ].insertBefore( paragraph, null );
571            } 
572
573            if( select && anchor.nodeType == Node.TEXT_NODE && !html.match( /<[a-z][a-z]*\s/i ) ) { // Consider improving.           
574                range.setStart( anchor, selection.anchorOffset );
575                var insertNode = this.document.createTextNode( html );
576                range.insertNode( insertNode );
577                var inserted = insertNode;
578            } else {
579                var pS = anchor.previousSibling; // Cache for check below:
580                var nS = anchor.nextSibling;
581                this.document.execCommand( "insertHTML", false, html );
582                if( pS !== anchor.previousSibling ) // We simply check what changed to find the target to select.
583                    inserted = anchor.previousSibling;
584                else if( nS !== anchor.nextSibling )   
585                    inserted = anchor.nextSibling;
586                else 
587                    inserted = anchor.firstChild;                 
588            }
589
590            if( defined( id ) ) // Final optional override of 'inserted' value:
591                inserted = this.document.getElementById( id );
592            if( inserted && inserted.tagName && inserted.tagName.toLowerCase() == "a" ) 
593                this.tagJustInserted = true; // See class-level jsdoc.
594            if( select ) {
595                range.selectNode( inserted );
596                this.monitorSelection(); // Required for Mozilla for proper arrow keys on highlighted link.
597            }
598            selection.addRange( range );       
599        }
600
601        if( isTempId && inserted ) {
602            inserted.id = undefined; // For IE 6.     
603            inserted.removeAttribute( "id" );
604        }
605
606        this.endCommand();
607
608        return inserted; 
609    },
610
611
612    /**
613     * class: <code>Editor.Iframe</code><br/>
614     * Set the caret outside of the first inline ancestor element, if any, of the starting container of the
615     * first range in the current selection <em>if</em> the user has tried (via the arrow or mousebutton)
616     * to get out of the element.  For example, put the caret outside of a link (anchor element)
617     * when the user is at an edge of the link.  This method is needed since FF 1+ will not otherwise easily
618     * "end" hyperlinks and other inline containers as the user is typing.
619     * @param selection <code>Selection</code>  The current selection.
620     * @param range <code>Range</code> OPTIONAL The first range from the current selection
621     * @param arrowKey <code>String</code> OPTIONAL If supplied, value must be 'left' or 'right'.  As a workaround
622     *                 for FF 1.5, when 'tagJustInserted' is <code>true</code> (see jsdoc above), method will
623     *                 put the caret to the side specified by 'arrowKey' of the first inline ancestor element, if any.
624     * @param mouseUp <code>boolean</code> OPTIONAL Whether the user has just moused-up. 
625     */
626    setCaretOutsideElement: function( selection, range, arrowKey, mouseUp ) {
627        if( selection.rangeCount ) { // w3c (Mozilla, etc)
628            if( !range ) 
629                range = selection.getRangeAt( 0 ).cloneRange();
630
631                var node = range.startContainer;
632                var nodeLength = node.data ? node.data.length : node.childNodes.length;               
633                var element = this.getFirstAncestorElementByDisplayType( node, true, true );           
634     
635                if( element && DOM.isInlineNode( element ) ) {
636                    if( !arrowKey && !mouseUp )
637                        return;
638                    if( ( ( arrowKey == "left" && this.tagJustInserted ) || range.startOffset == 0 ) && 
639                        !node.previousSibling ) { // 'tagJustInserted' is a workaround for FF 1.5; see jsdoc above.
640                        var t = ( element.previousSibling && element.previousSibling.nodeType == Node.TEXT_NODE )
641                                ? element.previousSibling : element.ownerDocument.createTextNode( "" );
642                        element.parentNode.insertBefore( t, element );
643                        range.setStart( t, t.data.length );
644                        range.collapse( true );
645                    } else if( ( ( arrowKey == "right"  && this.tagJustInserted )  || 
646                                   range.endOffset >= nodeLength ) && !node.nextSibling ) {
647                        var t = ( element.nextSibling && element.nextSibling.nodeType == Node.TEXT_NODE )
648                                ? element.nextSibling : element.ownerDocument.createTextNode( "" );
649                        element.parentNode.insertBefore( t, element.nextSibling );
650                        range.setEnd( t, 0 );
651                        range.collapse( false );
652                    }                       
653                }
654            }
655    },
656
657   
658    isCaretAtEnd: function( selection, range ) {
659        var focusNode = range.endContainer;
660
661        // Return 'false' if there exists a following node:   
662        if( focusNode.nextSibling )
663            return false;
664        var node = focusNode;
665        while ( node && node.parentNode && 
666                ( !node.parentNode.tagName || node.parentNode.tagName.toLowerCase() != "body" ) ) {
667            node = node.parentNode;
668            if( node.nextSibling )
669                return false;
670        }   
671       
672        if( focusNode.nodeType == Node.TEXT_NODE ) { 
673            if( range.endOffSet < focusNode.nodeValue.length )
674                return false; // 'false' if text node and not at end of text.
675        }
676
677        return true;
678    },
679
680
681    getFirstAncestorElementByDisplayType: function( element, inline, includeSelf ) {
682        var ancestors = DOM.getAncestors( element, includeSelf );
683        for( var i = 0; i < ancestors.length; i++ ) {
684            if( ancestors[ i ].nodeType == Node.TEXT_NODE )
685                continue;     
686            if( inline && DOM.isInlineNode( ancestors[ i ] ) || 
687                 !inline && !DOM.isInlineNode( ancestors[ i ] ) )                         
688                return ancestors[ i ];
689        }
690    },
691   
692   
693    isTextSelected: function() { 
694        var selection = this.getSelection();
695        if( !selection )
696            return;
697        if( (selection.type && selection.type == "Text") ||     // IE 6
698            (selection.toString && (selection + '').length) )   // w3c
699            return true;
700    },
701   
702   
703    getSelectedLink: function() {
704        var selection = this.getSelection();
705        var selectionRange = new SelectionRange( selection );
706        return DOM.getFirstAncestorByTagName( selectionRange.getCommonAncestorContainer(), "a" ) ||
707            DOM.filterElementsByTagName( selectionRange.getNodes(), "a" )[ 0 ];
708    },
709
710
711    /* commands */
712
713    /**
714     * class: <code>Editor.Iframe</code><br/>
715     * Prepares the <code>body</code> element with the appropriate css classname.
716     */
717    beginCommand: function() {
718        DOM.addClassName( this.document.body, "editor-transient" );
719    },
720
721
722    /**
723     * class: <code>Editor.Iframe</code><br/>
724     * Removes the appropriate css classname from the <code>body</code> element.
725     */
726    endCommand: function() {
727        DOM.removeClassName( this.document.body, "editor-transient" );
728    },
729
730
731    /**
732     * class: <code>Editor.Iframe</code><br/>
733     * @param command  <code>string</code> A valid rich-text-editor command identifier.  These can be
734     * either application-specific (non-native) commands, or native commands.  The native commands are
735     * identified and defined in the user-agent OEM <a
736     * href="http://msdn.microsoft.com/workshop/author/dhtml/reference/commandids.asp">documentation</a>
737     * on msdn.com for the Internet Explorer browser, which is a de-facto standard followed by the other
738     * rich-text editor, and in the Mozilla <a
739     * href="http://www.mozilla.org/editor/midas-spec.html">Midas documentation.</a><br/>
740     * Note:   The result of the execCommand operation does return a boolean value (<code>true</code> if
741     * successful), but this is not wired currently.
742     * @param userInterface  <code>boolean</code> See the documentation linked above.
743     * @param argument  <code>string</code>  See the documentation linked above.
744     */
745    execCommand: function( command, userInterface, argument ) {
746        this.beginCommand();
747        this.restoreSelection(); 
748       
749        log( command );
750       
751        switch( command ) {
752            case "unlink":
753                this.commandUnlink( argument );
754                break;
755           
756            case "createLink":
757                if( command == "createLink" ) {
758                    var selection = this.getSelection();           
759                    this.tagJustInserted = true; // See jsdoc.
760                }
761           
762            default:     
763                this.extendedExecCommand( command, userInterface, argument );
764        }
765       
766        this.monitorSelection();       
767        this.endCommand();           
768        this.editor.setChanged();
769    },
770
771
772    /**
773     * Override this method with extended <code>execCommand</code> functionality, if any.
774     * @param command  <code>string</code> A valid rich-text-editor command identifier.  See the
775     *                 documentation of the <code>execCommand</code> method.
776     * @param userInterface  <code>boolean</code> See the documentation linked above.
777     * @param argument  <code>string</code>  See the documentation linked above.
778     */
779    extendedExecCommand: function( command, userInterface, argument ) {     
780        this.document.execCommand( command, userInterface, argument );
781    },
782   
783   
784    commandUnlink: function( argument ) {
785        var element = this.getSelectedLink();
786        if( !element )
787            return false;
788       
789        var selection = this.getSelection();
790        if( selection.getRangeAt ) {
791            var range = selection.getRangeAt( 0 );
792            range.selectNode( element ); 
793        } else if( element.select )
794            element.select();
795       
796        this.document.execCommand( "unlink", false, null );
797    },
798   
799
800    formatBlock: function() {
801        this.document.execCommand( "formatBlock", false, this.FORMAT_BLOCK_TAG ); 
802    }
803} );
Note: See TracBrowser for help on using the browser.