Make WordPress Core

Changeset 27159


Ignore:
Timestamp:
02/10/2014 11:47:34 PM (11 years ago)
Author:
azaozz
Message:

Edit image in TinyMCE:

  • Add a "toolbar" at the top of the image with two buttons: Edit and Delete.
  • Don't open the edit modal on second click on the image. Makes the "edit" button somewhat pointless and can sometimes trigger after resizing the image.
  • When the image has caption: attempt to prevent IE11 from making the caption element resizable and wrapping it in thick border.
  • When the caret is inside a caption next to the image, pressing Enter will create a new <p> tag above the caption.
  • Hide the image toolbar on drag, cut, align.

Props gcorne, see #24409.

Location:
trunk/src/wp-includes
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-editor.php

    r27093 r27159  
    208208
    209209                if ( $set['teeny'] ) {
    210                     self::$plugins = $plugins = apply_filters( 'teeny_mce_plugins', array( 'fullscreen', 'link', 'image', 'wordpress', 'wplink' ), $editor_id );
     210                    self::$plugins = $plugins = apply_filters( 'teeny_mce_plugins', array( 'fullscreen', 'link', 'image', 'wordpress', 'wpeditimage', 'wplink' ), $editor_id );
    211211                } else {
    212212                    /**
     
    336336                }
    337337
    338                 // WordPress default stylesheet
    339                 $mce_css = array( self::$baseurl . '/skins/wordpress/wp-content.css' );
     338                $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
     339                $version = 'ver=' . $GLOBALS['wp_version'];
     340                $dashicons = includes_url( "css/dashicons$suffix.css?$version" );
     341
     342                // WordPress default stylesheet and dashicons
     343                $mce_css = array( $dashicons, self::$baseurl . '/skins/wordpress/wp-content.css' );
    340344
    341345                // load editor_style.css if the current theme supports it
  • trunk/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js

    r27098 r27159  
    4343            width = parseInt( w, 10 ) + 10;
    4444
    45             return '<div class="mceTemp" draggable="true"><dl id="'+ id +'" class="wp-caption '+ cls +'" style="width: '+ width +'px">' +
     45            return '<div class="mceTemp"><dl id="'+ id +'" class="wp-caption '+ cls +'" style="width: '+ width +'px">' +
    4646                '<dt class="wp-caption-dt">'+ img +'</dt><dd class="wp-caption-dd">'+ cap +'</dd></dl></div>';
    4747        });
     
    103103
    104104    function extractImageData( imageNode ) {
    105         var classes, metadata, captionBlock, caption;
     105        var classes, metadata, captionBlock, caption,
     106            dom = editor.dom;
    106107
    107108        // default attributes
     
    119120        };
    120121
    121         metadata.url = editor.dom.getAttrib( imageNode, 'src' );
    122         metadata.alt = editor.dom.getAttrib( imageNode, 'alt' );
    123         metadata.width = parseInt( editor.dom.getAttrib( imageNode, 'width' ), 10 );
    124         metadata.height = parseInt( editor.dom.getAttrib( imageNode, 'height' ), 10 );
    125 
    126         //TODO: probably should capture attributes on both the <img /> and the <a /> so that they can be restored when the image and/or caption are updated
     122        metadata.url = dom.getAttrib( imageNode, 'src' );
     123        metadata.alt = dom.getAttrib( imageNode, 'alt' );
     124        metadata.width = parseInt( dom.getAttrib( imageNode, 'width' ), 10 );
     125        metadata.height = parseInt( dom.getAttrib( imageNode, 'height' ), 10 );
     126
     127        //TODO: probably should capture attributes on both the <img /> and the <a /> so that they can be restored
     128        // when the image and/or caption are updated
    127129        // maybe use getAttribs()
    128130
     
    145147
    146148        // extract caption
    147         captionBlock = editor.dom.getParents( imageNode, '.wp-caption' );
     149        captionBlock = dom.getParents( imageNode, '.wp-caption' );
    148150
    149151        if ( captionBlock.length ) {
     
    156158                }
    157159            } );
    158             caption = editor.dom.select( 'dd.wp-caption-dd', captionBlock );
     160
     161            caption = dom.select( 'dd.wp-caption-dd', captionBlock );
    159162            if ( caption.length ) {
    160163                caption = caption[0];
     
    162165                metadata.caption = editor.serializer.serialize( caption )
    163166                    .replace( /<br[^>]*>/g, '$&\n' ).replace( /^<p>/, '' ).replace( /<\/p>$/, '' );
    164 
    165167            }
    166168        }
    167169
    168170        // extract linkTo
    169         if ( imageNode.parentNode.nodeName === 'A' ) {
    170             metadata.linkUrl = editor.dom.getAttrib( imageNode.parentNode, 'href' );
     171        if ( imageNode.parentNode && imageNode.parentNode.nodeName === 'A' ) {
     172            metadata.linkUrl = dom.getAttrib( imageNode.parentNode, 'href' );
    171173        }
    172174
     
    175177
    176178    function updateImage( imageNode, imageData ) {
    177         var className, width, node, html, captionNode, nodeToReplace, uid;
     179        var className, width, node, html, captionNode, nodeToReplace, uid, editedImg;
    178180
    179181        if ( imageData.caption ) {
     
    186188            //TODO: shouldn't add the id attribute if it isn't an attachment
    187189
    188             // should create a new function for genrating the caption markup
     190            // should create a new function for generating the caption markup
    189191            html =  '<dl id="'+ imageData.attachment_id +'" class="wp-caption '+ className +'" style="width: '+ width +'px">' +
    190192                '<dt class="wp-caption-dt">'+ html + '</dt><dd class="wp-caption-dd">'+ imageData.caption +'</dd></dl>';
    191193
    192             node = editor.dom.create( 'div', { 'class': 'mceTemp', draggable: 'true' }, html );
     194            node = editor.dom.create( 'div', { 'class': 'mceTemp' }, html );
    193195        } else {
    194196            node = createImageAndLink( imageData, 'node' );
     
    216218        editor.dom.setAttrib( node, 'data-wp-replace-id', '' );
    217219
    218         if ( node.nodeName === 'IMG' ) {
    219             editor.selection.select( node );
    220         } else {
    221             editor.selection.select( editor.dom.select( 'img', node )[0] );
    222         }
    223220        editor.nodeChanged();
     221
     222        editedImg = node.nodeName === 'IMG' ? node : editor.dom.select( 'img', node )[0];
     223
     224        if ( editedImg ) {
     225            editor.selection.select( editedImg );
     226            // refresh toolbar
     227            addToolbar( editedImg );
     228        }
    224229    }
    225230
     
    265270    }
    266271
     272    function editImage( img ) {
     273        var frame, callback;
     274
     275        if ( typeof wp === 'undefined' || ! wp.media ) {
     276            editor.execCommand( 'mceImage' );
     277            return;
     278        }
     279
     280        editor.undoManager.add();
     281
     282        frame = wp.media({
     283            frame: 'image',
     284            state: 'image-details',
     285            metadata: extractImageData( img )
     286        } );
     287
     288        callback = function( imageData ) {
     289            updateImage( img, imageData );
     290            editor.focus();
     291        };
     292
     293        frame.state('image-details').on( 'update', callback );
     294        frame.state('replace-image').on( 'replace', callback );
     295        frame.on( 'close', function() {
     296            editor.focus();
     297    //      editor.selection.select( img );
     298    //      editor.nodeChanged();
     299        });
     300
     301        frame.open();
     302    }
     303
     304    function removeImage( node ) {
     305        var wrap;
     306
     307        if ( node.nodeName === 'DIV' && editor.dom.hasClass( node, 'mceTemp' ) ) {
     308            wrap = node;
     309        } else if ( node.nodeName === 'IMG' || node.nodeName === 'DT' || node.nodeName === 'A' ) {
     310            wrap = editor.dom.getParent( node, 'div.mceTemp' );
     311        }
     312
     313        if ( wrap ) {
     314            if ( wrap.nextSibling ) {
     315                editor.selection.select( wrap.nextSibling );
     316            } else if ( wrap.previousSibling ) {
     317                editor.selection.select( wrap.previousSibling );
     318            } else {
     319                editor.selection.select( wrap.parentNode );
     320            }
     321
     322            editor.selection.collapse( true );
     323            editor.nodeChanged();
     324            editor.dom.remove( wrap );
     325        } else {
     326            editor.dom.remove( node );
     327        }
     328    }
     329
     330    function addToolbar( node ) {
     331        var position, toolbarHtml, toolbar,
     332            dom = editor.dom;
     333
     334        removeToolbar();
     335
     336        // Don't add to placeholders
     337        if ( ! node || node.nodeName !== 'IMG' || isPlaceholder( node ) ) {
     338            return;
     339        }
     340
     341        dom.setAttrib( node, 'data-wp-imgselect', 1 );
     342        position = dom.getPos( node, editor.getBody() );
     343
     344        toolbarHtml = '<div class="wrapper" data-mce-bogus="1">' +
     345            '<div class="dashicons dashicons-format-image edit" data-mce-bogus="1"></div> ' +
     346            '<div class="dashicons dashicons-no-alt remove" data-mce-bogus="1"></div></div>';
     347
     348        toolbar = dom.create( 'div', {
     349            'id': 'wp-image-toolbar',
     350            'data-mce-bogus': '1',
     351            'contenteditable': false
     352        }, toolbarHtml );
     353
     354        editor.getBody().appendChild( toolbar );
     355
     356        dom.setStyles( toolbar, {
     357            top: position.y,
     358            left: position.x,
     359            width: node.width
     360        });
     361    }
     362
     363    function removeToolbar() {
     364        var toolbar = editor.dom.get( 'wp-image-toolbar' );
     365
     366        if ( toolbar ) {
     367            editor.dom.remove( toolbar );
     368        }
     369
     370        editor.dom.setAttrib( editor.dom.select( 'img[data-wp-imgselect]' ), 'data-wp-imgselect', null );
     371    }
     372
     373    function isPlaceholder( node ) {
     374        var dom = editor.dom;
     375
     376        if ( dom.hasClass( node, 'mceItem' ) || dom.getAttrib( node, 'data-mce-placeholder' ) ||
     377            dom.getAttrib( node, 'data-mce-object' ) ) {
     378
     379            return true;
     380        }
     381
     382        return false;
     383    }
     384
    267385    editor.on( 'init', function() {
    268386        var dom = editor.dom;
    269387
    270388        // Add caption field to the default image dialog
    271         editor.on( 'wpLoadImageForm', function( e ) {
     389        editor.on( 'wpLoadImageForm', function( event ) {
    272390            if ( editor.getParam( 'wpeditimage_disable_captions' ) ) {
    273391                return;
     
    284402            };
    285403
    286             e.data.splice( e.data.length - 1, 0, captionField );
     404            event.data.splice( event.data.length - 1, 0, captionField );
    287405        });
    288406
    289407        // Fix caption parent width for images added from URL
    290         editor.on( 'wpNewImageRefresh', function( e ) {
     408        editor.on( 'wpNewImageRefresh', function( event ) {
    291409            var parent, captionWidth;
    292410
    293             if ( parent = dom.getParent( e.node, 'dl.wp-caption' ) ) {
     411            if ( parent = dom.getParent( event.node, 'dl.wp-caption' ) ) {
    294412                if ( ! parent.style.width ) {
    295                     captionWidth = parseInt( e.node.clientWidth, 10 ) + 10;
     413                    captionWidth = parseInt( event.node.clientWidth, 10 ) + 10;
    296414                    captionWidth = captionWidth ? captionWidth + 'px' : '50%';
    297415                    dom.setStyle( parent, 'width', captionWidth );
     
    300418        });
    301419
    302         editor.on( 'wpImageFormSubmit', function( e ) {
    303             var data = e.imgData.data,
    304                 imgNode = e.imgData.node,
    305                 caption = e.imgData.caption,
     420        editor.on( 'wpImageFormSubmit', function( event ) {
     421            var data = event.imgData.data,
     422                imgNode = event.imgData.node,
     423                caption = event.imgData.caption,
    306424                captionId = '',
    307425                captionAlign = '',
     
    312430            data.id = '__wp-temp-img-id';
    313431            // Cancel the original callback
    314             e.imgData.cancel = true;
     432            event.imgData.cancel = true;
    315433
    316434            if ( ! data.style ) {
     
    366484
    367485                    if ( parent && parent.nodeName === 'P' ) {
    368                         wrap = dom.create( 'div', { 'class': 'mceTemp', 'draggable': 'true' }, html );
     486                        wrap = dom.create( 'div', { 'class': 'mceTemp' }, html );
    369487                        dom.insertAfter( wrap, parent );
    370488                        editor.selection.select( wrap );
     
    375493                        }
    376494                    } else {
    377                         editor.selection.setContent( '<div class="mceTemp" draggable="true">' + html + '</div>' );
     495                        editor.selection.setContent( '<div class="mceTemp">' + html + '</div>' );
    378496                    }
    379497                } else {
     
    432550
    433551                        if ( parent = dom.getParent( imgNode, 'p' ) ) {
    434                             wrap = dom.create( 'div', { 'class': 'mceTemp', 'draggable': 'true' }, html );
     552                            wrap = dom.create( 'div', { 'class': 'mceTemp' }, html );
    435553                            dom.insertAfter( wrap, parent );
    436554                            editor.selection.select( wrap );
     
    444562                            }
    445563                        } else {
    446                             editor.selection.setContent( '<div class="mceTemp" draggable="true">' + html + '</div>' );
     564                            editor.selection.setContent( '<div class="mceTemp">' + html + '</div>' );
    447565                        }
    448566                    }
     
    467585            imgNode = dom.get('__wp-temp-img-id');
    468586            dom.setAttrib( imgNode, 'id', imgId );
    469             e.imgData.node = imgNode;
     587            event.imgData.node = imgNode;
    470588        });
    471589
    472         editor.on( 'wpLoadImageData', function( e ) {
     590        editor.on( 'wpLoadImageData', function( event ) {
    473591            var parent,
    474                 data = e.imgData.data,
    475                 imgNode = e.imgData.node;
     592                data = event.imgData.data,
     593                imgNode = event.imgData.node;
    476594
    477595            if ( parent = dom.getParent( imgNode, 'dl.wp-caption' ) ) {
     
    485603        });
    486604
    487         // Prevent dragging images out of the caption elements
    488605        dom.bind( editor.getDoc(), 'dragstart', function( event ) {
    489606            var node = editor.selection.getNode();
    490607
     608            // Prevent dragging images out of the caption elements
    491609            if ( node.nodeName === 'IMG' && dom.getParent( node, '.wp-caption' ) ) {
    492610                event.preventDefault();
    493611            }
     612
     613            // Remove toolbar to avoid an orphaned toolbar when dragging an image to a new location
     614            removeToolbar();
     615
    494616        });
     617
     618        // Prevent IE11 from making dl.wp-caption resizable
     619        if ( tinymce.Env.ie && tinymce.Env.ie > 10 ) {
     620            // The 'mscontrolselect' event is supported only in IE11+
     621            dom.bind( editor.getBody(), 'mscontrolselect', function( event ) {
     622                if ( event.target.nodeName === 'IMG' && dom.getParent( event.target, '.wp-caption' ) ) {
     623                    // Hide the thick border with resize handles around dl.wp-caption
     624                    editor.getBody().focus(); // :(
     625                } else if ( event.target.nodeName === 'DL' && dom.hasClass( event.target, 'wp-caption' ) ) {
     626                    // Trigger the thick border with resize handles...
     627                    // This will make the caption text editable.
     628                    event.target.focus();
     629                }
     630            });
     631
     632            editor.on( 'click', function( event ) {
     633                if ( event.target.nodeName === 'IMG' && dom.getAttrib( event.target, 'data-wp-imgselect' ) &&
     634                    dom.getParent( event.target, 'dl.wp-caption' ) ) {
     635
     636                    editor.getBody().focus();
     637                }
     638            });
     639        }
    495640    });
    496641
     
    499644            node = event.target;
    500645
    501         if ( node.nodeName === 'IMG' && ( parent = editor.dom.getParent( node, '.wp-caption' ) ) ) {
    502             width = event.width || editor.dom.getAttrib( node, 'width' );
    503 
    504             if ( width ) {
    505                 width = parseInt( width, 10 ) + 10;
    506                 editor.dom.setStyle( parent, 'width', width + 'px' );
    507             }
     646        if ( node.nodeName === 'IMG' ) {
     647            if ( parent = editor.dom.getParent( node, '.wp-caption' ) ) {
     648                width = event.width || editor.dom.getAttrib( node, 'width' );
     649
     650                if ( width ) {
     651                    width = parseInt( width, 10 ) + 10;
     652                    editor.dom.setStyle( parent, 'width', width + 'px' );
     653                }
     654            }
     655            // refresh toolbar
     656            addToolbar( node );
    508657        }
    509658    });
    510659
    511     editor.on( 'BeforeExecCommand', function( e ) {
     660    editor.on( 'BeforeExecCommand', function( event ) {
    512661        var node, p, DL, align,
    513             cmd = e.command,
     662            cmd = event.command,
    514663            dom = editor.dom;
    515664
     
    526675                    setTimeout( function() {
    527676                        editor.selection.setCursorLocation( p, 0 );
    528                         editor.selection.setContent( e.value );
     677                        editor.selection.setContent( event.value );
    529678                    }, 500 );
    530679
     
    536685            align = cmd.substr(7).toLowerCase();
    537686            align = 'align' + align;
     687
     688            removeToolbar();
    538689
    539690            if ( dom.is( node, 'dl.wp-caption' ) ) {
     
    567718    });
    568719
    569     editor.on( 'keydown', function( e ) {
     720    editor.on( 'keydown', function( event ) {
    570721        var node, wrap, P, spacer,
    571722            selection = editor.selection,
    572723            dom = editor.dom;
    573724
    574         if ( e.keyCode === tinymce.util.VK.ENTER ) {
     725        if ( event.keyCode === tinymce.util.VK.ENTER ) {
    575726            // When pressing Enter inside a caption move the caret to a new parapraph under it
    576             wrap = dom.getParent( editor.selection.getNode(), 'div.mceTemp' );
     727            node = selection.getNode();
     728            wrap = dom.getParent( node, 'div.mceTemp' );
    577729
    578730            if ( wrap ) {
    579                 dom.events.cancel(e); // Doesn't cancel all :(
     731                dom.events.cancel( event ); // Doesn't cancel all :(
    580732
    581733                // Remove any extra dt and dd cleated on pressing Enter...
     
    586738                });
    587739
    588                 spacer = tinymce.Env.ie ? '' : '<br data-mce-bogus="1" />';
     740                spacer = tinymce.Env.ie && tinymce.Env.ie < 11 ? '' : '<br data-mce-bogus="1" />';
    589741                P = dom.create( 'p', null, spacer );
    590                 dom.insertAfter( P, wrap );
     742
     743                if ( node.nodeName === 'DD' ) {
     744                    dom.insertAfter( P, wrap );
     745                } else {
     746                    wrap.parentNode.insertBefore( P, wrap );
     747                }
     748
     749                editor.nodeChanged();
    591750                selection.setCursorLocation( P, 0 );
    592                 editor.nodeChanged();
    593             }
    594         } else if ( e.keyCode === tinymce.util.VK.DELETE || e.keyCode === tinymce.util.VK.BACKSPACE ) {
     751            }
     752        } else if ( event.keyCode === tinymce.util.VK.DELETE || event.keyCode === tinymce.util.VK.BACKSPACE ) {
    595753            node = selection.getNode();
    596754
     
    602760
    603761            if ( wrap ) {
    604                 dom.events.cancel(e);
    605 
    606                 if ( wrap.nextSibling ) {
    607                     selection.select( wrap.nextSibling );
    608                 } else if ( wrap.previousSibling ) {
    609                     selection.select( wrap.previousSibling );
    610                 } else {
    611                     selection.select( wrap.parentNode );
    612                 }
    613 
    614                 selection.collapse( true );
    615                 editor.nodeChanged();
    616                 dom.remove( wrap );
    617                 wrap = null;
     762                dom.events.cancel( event );
     763                removeImage( node );
    618764                return false;
    619765            }
     
    621767    });
    622768
    623     editor.on( 'mousedown', function( e ) {
    624         var imageNode, frame, callback;
    625         if ( e.target.nodeName === 'IMG' && editor.selection.getNode() === e.target ) {
    626             // Don't trigger on right-click
    627             if ( e.button !== 2 ) {
    628 
    629                 // Don't attempt to edit placeholders
    630                 if ( editor.dom.hasClass( e.target, 'mceItem' ) || '1' === editor.dom.getAttrib( e.target, 'data-mce-placeholder' ) ) {
    631                     return;
    632                 }
    633 
    634                 imageNode = e.target;
    635 
    636                 frame = wp.media({
    637                     frame: 'image',
    638                     state: 'image-details',
    639                     metadata: extractImageData( imageNode )
    640                 } );
    641 
    642                 callback = function( imageData ) {
    643                     updateImage( imageNode, imageData );
    644                     editor.focus();
    645                 };
    646 
    647                 frame.state('image-details').on( 'update', callback );
    648                 frame.state('replace-image').on( 'replace', callback );
    649 
    650                 frame.open();
    651             }
    652         }
    653     } );
     769    editor.on( 'mousedown', function( event ) {
     770        var node = event.target;
     771
     772        if ( tinymce.Env.ie && editor.dom.getParent( node, '#wp-image-toolbar' ) ) {
     773            // Stop IE > 8 from making the wrapper resizable on mousedown
     774            event.preventDefault();
     775        }
     776
     777        if ( node.nodeName === 'IMG' && ! editor.dom.getAttrib( node, 'data-wp-imgselect' ) && ! isPlaceholder( node ) ) {
     778            addToolbar( node );
     779        }
     780    });
     781
     782    editor.on( 'mouseup', function( event ) {
     783        var image,
     784            node = event.target,
     785            dom = editor.dom;
     786
     787        // Don't trigger on right-click
     788        if ( event.button && event.button > 1 ) {
     789            return;
     790        }
     791
     792        if ( node.nodeName === 'DIV' && dom.getParent( node, '#wp-image-toolbar' ) ) {
     793            image = dom.select( 'img[data-wp-imgselect]' )[0];
     794
     795            if ( image ) {
     796                editor.selection.select( image );
     797
     798                if ( dom.hasClass( node, 'remove' ) ) {
     799                    removeImage( image );
     800                    removeToolbar();
     801                } else if ( dom.hasClass( node, 'edit' ) ) {
     802                    editImage( image );
     803                }
     804            }
     805        } else if ( node.nodeName !== 'IMG' ) {
     806            removeToolbar();
     807        }
     808    });
     809
     810    editor.on( 'cut', function() {
     811        removeToolbar();
     812    });
    654813
    655814    editor.wpSetImgCaption = function( content ) {
     
    661820    };
    662821
    663     editor.on( 'BeforeSetContent', function( e ) {
    664         e.content = editor.wpSetImgCaption( e.content );
     822    editor.on( 'BeforeSetContent', function( event ) {
     823        event.content = editor.wpSetImgCaption( event.content );
    665824    });
    666825
    667     editor.on( 'PostProcess', function( e ) {
    668         if ( e.get ) {
    669             e.content = editor.wpGetImgCaption( e.content );
     826    editor.on( 'PostProcess', function( event ) {
     827        if ( event.get ) {
     828            event.content = editor.wpGetImgCaption( event.content );
     829            event.content = event.content.replace( / data-wp-imgselect="1"/g, '' );
    670830        }
    671831    });
  • trunk/src/wp-includes/js/tinymce/skins/wordpress/wp-content.css

    r26880 r27159  
    118118}
    119119
     120#wp-image-toolbar {
     121    position: absolute;
     122}
     123
     124#wp-image-toolbar .wrapper {
     125    position: relative;
     126    height: 33px;
     127    background-color: rgba(0,0,0,0.3);
     128}
     129
     130#wp-image-toolbar .dashicons {
     131    position: absolute;
     132    color: white;
     133    width: 36px;
     134    height: 32px;
     135    line-height: 32px;
     136    cursor: pointer;
     137}
     138
     139#wp-image-toolbar div.dashicons-no-alt {
     140    top: 0;
     141    right: 0;
     142}
     143
     144#wp-image-toolbar div.dashicons-format-image {
     145    top: 0;
     146    left: 0;
     147}
     148
     149/* Image resize handles */
     150.mce-content-body div.mce-resizehandle {
     151    border-color: #777;
     152    width: 7px;
     153    height: 7px;
     154}
     155
     156.mce-content-body img[data-mce-selected] {
     157    outline: 1px solid #777;
     158}
     159
    120160.mce-content-body img.wp-gallery:hover {
    121161    background-color: #ededed;
Note: See TracChangeset for help on using the changeset viewer.