Make WordPress Core

Changeset 38967


Ignore:
Timestamp:
10/26/2016 08:02:51 PM (10 years ago)
Author:
westonruter
Message:

Customize: Add edit shortcuts in customizer preview to visually expose editable elements and focus on the corresponding controls when clicked.

  • Edit shortcuts show initially for a moment and then fade away so as to not get in the way of the preview.
  • Visibility of edit shortcuts is toggled by clicking/touching anywhere inert in the document.
  • Implements UI for mobile and touch devices which do not support shift-click.
  • Adds editShortcutVisibility state.
  • Adds new methods to wp.customize.selectiveRefresh.Partial for managing edit shortcuts.

Incorporates aspects of the Customize Direct Manipulation feature plugin.

Props sirbrillig, mattwiebe, celloexpressions, melchoyce, westonruter, afercia.
Fixes #27403.

Location:
trunk/src
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/customize.php

    r38813 r38967  
    162162                                </div>
    163163                                <div class="customize-panel-description"><?php
    164                                         _e( 'The Customizer allows you to preview changes to your site before publishing them. You can also navigate to different pages on your site to preview them.' );
     164                                        _e( 'The Customizer allows you to preview changes to your site before publishing them. You can navigate to different pages on your site within the preview. Edit shortcuts are shown for some editable elements; click anywhere in the preview to toggle them off and on.' );
    165165                                ?></div>
    166166                        </div>
  • trunk/src/wp-admin/js/customize-controls.js

    r38948 r38967  
    43034303                                synced.scroll = previewer.scroll;
    43044304                        }
     4305                        synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
    43054306                        previewer.send( 'sync', synced );
    43064307
     
    51235124                                changesetStatus = state.create( 'changesetStatus' ),
    51245125                                previewerAlive = state.create( 'previewerAlive' ),
     5126                                editShortcutVisibility  = state.create( 'editShortcutVisibility' ),
    51255127                                populateChangesetUuidParam;
    51265128
     
    51595161                        expandedSection( false );
    51605162                        previewerAlive( true );
     5163                        editShortcutVisibility( 'initial' );
    51615164                        changesetStatus( api.settings.changeset.status );
    51625165
     
    57525755                });
    57535756
     5757                // Update the edit shortcut visibility state.
     5758                api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
     5759                        api.state( 'editShortcutVisibility' ).set( visibility );
     5760                } );
     5761                api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
     5762                        api.previewer.send( 'edit-shortcut-visibility', visibility );
     5763                } );
     5764
    57545765                // Autosave changeset.
    57555766                ( function() {
  • trunk/src/wp-content/themes/twentyfourteen/style.css

    r38171 r38967  
    10401040}
    10411041
     1042.secondary-navigation .customize-partial-edit-shortcut:before,
     1043.footer-sidebar .widget:first-child .customize-partial-edit-shortcut:before {
     1044        left: 0;
     1045}
    10421046
    10431047/**
  • trunk/src/wp-includes/css/customize-preview.css

    r36890 r38967  
    1111        box-shadow: none;
    1212}
     13
     14/* Make shortcut buttons essentially invisible */
     15.widget button.customize-partial-edit-shortcut,
     16.customize-partial-edit-shortcut {
     17        position: relative;
     18        float: left;
     19        width: 1px; /* required to have a size to be focusable in Safari */
     20        height: 1px;
     21        padding: 0;
     22        margin: -1px 0 0 -1px;
     23        border: 0;
     24        background: transparent;
     25        color: transparent;
     26        box-shadow: none;
     27        outline: none;
     28        z-index: 5;
     29}
     30
     31.customize-partial-edit-shortcut:active {
     32        padding: 0;
     33        border: 0;
     34}
     35
     36/* Styles for the actual shortcut */
     37.customize-partial-edit-shortcut:before {
     38        position: absolute;
     39        left: -36px;
     40        color: #fff;
     41        width: 30px;
     42        height: 30px;
     43        font-size: 18px;
     44        font-family: dashicons;
     45        content: '\f464';
     46        z-index: 5;
     47        background-color: #0085ba;
     48        border-radius: 50%;
     49        border: 2px solid #fff;
     50        box-shadow: 0 2px 1px rgba(46,68,83,0.15);
     51        text-align: center;
     52        display: flex;
     53        flex-direction: row;
     54        justify-content: center;
     55        align-items: center;
     56        cursor: pointer;
     57        padding: 0;
     58        animation-fill-mode: both;
     59        animation-duration: .4s;
     60        opacity: 0;
     61        pointer-events: none;
     62        text-shadow: 0 -1px 1px #006799,
     63                     1px 0 1px #006799,
     64                     0 1px 1px #006799,
     65                     -1px 0 1px #006799;
     66}
     67
     68.customize-partial-edit-shortcut:hover:before,
     69.customize-partial-edit-shortcut:focus:before {
     70    background: #008ec2; /* matches primary buttons */
     71        border-color: #191e23; /* provides visual focus style with 4.5 contrast against background color */
     72}
     73
     74body.customize-partial-edit-shortcuts-shown .customize-partial-edit-shortcut:before {
     75        animation-name: customize-partial-edit-shortcut-bounce-appear;
     76        pointer-events: auto;
     77}
     78body.customize-partial-edit-shortcuts-hidden .customize-partial-edit-shortcut:before {
     79        animation-name: customize-partial-edit-shortcut-bounce-disappear;
     80        pointer-events: none;
     81}
     82
     83body.customize-partial-edit-shortcuts-flash .customize-partial-edit-shortcut:before {
     84        animation-duration: 1.5s;
     85        animation-delay: 0.4s;
     86        animation-name: customize-partial-edit-shortcut-bounce-disappear;
     87        pointer-events: none;
     88}
     89
     90.page-sidebar-collapsed .customize-partial-edit-shortcut:before,
     91.customize-partial-edit-shortcut-hidden:before {
     92        visibility: hidden;
     93}
     94
     95.widget button.customize-partial-edit-shortcut-absolute,
     96.customize-partial-edit-shortcut-absolute {
     97        position: static;
     98}
     99
     100.customize-partial-edit-shortcut-left-margin:before {
     101        left: 0;
     102}
     103
     104@keyframes customize-partial-edit-shortcut-bounce-appear {
     105        from, 20%, 40%, 60%, 80%, to {
     106                animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
     107        }
     108        0% {
     109                opacity: 0;
     110                transform: scale3d(.3, .3, .3);
     111        }
     112        20% {
     113                transform: scale3d(1.1, 1.1, 1.1);
     114        }
     115        40% {
     116                transform: scale3d(.9, .9, .9);
     117        }
     118        60% {
     119                opacity: 1;
     120                transform: scale3d(1.03, 1.03, 1.03);
     121        }
     122        80% {
     123                transform: scale3d(.97, .97, .97);
     124        }
     125        to {
     126                opacity: 1;
     127                transform: scale3d(1, 1, 1);
     128        }
     129}
     130
     131@keyframes customize-partial-edit-shortcut-bounce-disappear {
     132        from, 20%, 40%, 60%, 80%, to {
     133                animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
     134        }
     135        0% {
     136                opacity: 1;
     137                transform: scale3d(1, 1, 1);
     138        }
     139        20% {
     140                transform: scale3d(.97, .97, .97);
     141        }
     142        40% {
     143                opacity: 1;
     144                transform: scale3d(1.03, 1.03, 1.03);
     145        }
     146        60% {
     147                transform: scale3d(.9, .9, .9);
     148        }
     149        80% {
     150                transform: scale3d(1.1, 1.1, 1.1);
     151        }
     152        to {
     153                opacity: 0;
     154                transform: scale3d(.3, .3, .3);
     155        }
     156}
     157
     158@media screen and (max-width:800px) {
     159        .customize-partial-edit-shortcut:before {
     160                left: -18px; /* Assume there will be less of a margin available on smaller screens */
     161        }
     162}
  • trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php

    r38810 r38967  
    185185                        'l10n'           => array(
    186186                                'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
     187                                'clickEditMenu' => __( 'Click to edit this menu.' ),
     188                                'clickEditWidget' => __( 'Click to edit this widget.' ),
     189                                'clickEditTitle' => __( 'Click to edit the site title.' ),
     190                                'clickEditMisc' => __( 'Click to edit this element.' ),
     191                                'editShortcutVisibilityToggle'  => __( 'Toggle edit shortcuts' ),
    187192                                /* translators: %s: document.write() */
    188193                                'badDocumentWrite' => sprintf( __( '%s is forbidden' ), 'document.write()' ),
  • trunk/src/wp-includes/js/customize-preview-widgets.js

    r38810 r38967  
    358358                                        params: {}
    359359                                } );
    360                                 api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
    361360                        }
    362361
     
    400399                                wasInserted = true;
    401400                        } );
     401
     402                        api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
    402403
    403404                        if ( wasInserted ) {
  • trunk/src/wp-includes/js/customize-selective-refresh.js

    r38810 r38967  
    77        self = {
    88                ready: $.Deferred(),
     9                editShortcutVisibility: new api.Value(),
    910                data: {
    1011                        partials: {},
     
    4344                id: null,
    4445
    45                  /**
     46                /**
    4647                 * Constructor.
    4748                 *
     
    8384                ready: function() {
    8485                        var partial = this;
    85                         _.each( _.pluck( partial.placements(), 'container' ), function( container ) {
    86                                 $( container ).attr( 'title', self.data.l10n.shiftClickToEdit );
     86                        _.each( partial.placements(), function( placement ) {
     87                                $( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit );
     88                                partial.createEditShortcutForPlacement( placement );
    8789                        } );
    8890                        $( document ).on( 'click', partial.params.selector, function( e ) {
     
    100102
    101103                /**
     104                 * Create and show the edit shortcut for a given partial placement container.
     105                 *
     106                 * @since 4.7
     107                 *
     108                 * @param {Placement} placement The placement container element.
     109                 * @returns {void}
     110                 */
     111                createEditShortcutForPlacement: function( placement ) {
     112                        var partial = this, $shortcut, $placementContainer;
     113                        if ( ! placement.container ) {
     114                                return;
     115                        }
     116                        $placementContainer = $( placement.container );
     117                        if ( ! $placementContainer.length ) {
     118                                return;
     119                        }
     120                        $shortcut = partial.createEditShortcut();
     121                        partial.positionEditShortcut( placement, $shortcut );
     122                        $shortcut.on( 'click', function( event ) {
     123                                event.preventDefault();
     124                                event.stopPropagation();
     125                                partial.showControl();
     126                        } );
     127                },
     128
     129                /**
     130                 * Position an edit shortcut for the placement container.
     131                 *
     132                 * The shortcut must already be created and added to the page.
     133                 *
     134                 * @since 4.7
     135                 *
     136                 * @param {Placement} placement The placement for the partial.
     137                 * @param {jQuery} $editShortcut The shortcut element as a jQuery object.
     138                 * @returns {void}
     139                 */
     140                positionEditShortcut: function( placement, $editShortcut ) {
     141                        var partial = this, $placementContainer;
     142                        $placementContainer = $( placement.container );
     143                        $placementContainer.prepend( $editShortcut );
     144                        if ( 'absolute' === $placementContainer.css( 'position' ) ) {
     145                                $editShortcut.addClass( 'customize-partial-edit-shortcut-absolute' );
     146                                $editShortcut.css( partial.getEditShortcutPositionStyles( $placementContainer ) );
     147                                partial.whenPageChanges( function() {
     148                                        $editShortcut.css( partial.getEditShortcutPositionStyles( $placementContainer ) );
     149                                } );
     150                        }
     151                        if ( ! $placementContainer.is( ':visible' ) || 'none' === $placementContainer.css( 'display' ) ) {
     152                                $editShortcut.addClass( 'customize-partial-edit-shortcut-hidden' );
     153                        }
     154                        $editShortcut.toggleClass( 'customize-partial-edit-shortcut-left-margin', $editShortcut.offset().left < 1 );
     155                },
     156
     157                /**
     158                 * Call a callback function when the page changes.
     159                 *
     160                 * This calls a callback for any change that might require refreshing the edit shortcuts.
     161                 *
     162                 * @since 4.7
     163                 *
     164                 * @param {Function} callback The function to call when the page changes.
     165                 * @returns {void}
     166                 */
     167                whenPageChanges: function( callback ) {
     168                        var debouncedCallback, $document;
     169                        debouncedCallback = _.debounce( function() {
     170                                // Timeout allows any page animations to finish
     171                                setTimeout( callback, 100 );
     172                        }, 350 );
     173                        // When window is resized.
     174                        $( window ).resize( debouncedCallback );
     175                        // When any customizer setting changes.
     176                        api.bind( 'change', debouncedCallback );
     177                        $document = $( window.document );
     178                        // After scroll in case there are fixed position elements
     179                        $document.on( 'scroll', debouncedCallback );
     180                        // After page click (eg: hamburger menus)
     181                        $document.on( 'click', debouncedCallback );
     182                },
     183
     184                /**
     185                 * Return the CSS positioning for the edit shortcut for a given partial placement.
     186                 *
     187                 * @since 4.7
     188                 *
     189                 * @param {jQuery} $placementContainer The placement container element as a jQuery object.
     190                 * @return {Object} Object containing CSS positions.
     191                 */
     192                getEditShortcutPositionStyles: function( $placementContainer ) {
     193                        return {
     194                                top: $placementContainer.css( 'top' ),
     195                                left: $placementContainer.css( 'left' ),
     196                                right: 'auto'
     197                        };
     198                },
     199
     200                /**
     201                 * Return the unique class name for the edit shortcut button for this partial.
     202                 *
     203                 * @since 4.7
     204                 *
     205                 * @return {string} Partial ID converted into a class name for use in shortcut.
     206                 */
     207                getEditShortcutClassName: function() {
     208                        var partial = this, cleanId;
     209                        cleanId = partial.id.replace( /]/g, '' ).replace( /\[/g, '-' );
     210                        return 'customize-partial-edit-shortcut-' + cleanId;
     211                },
     212
     213                /**
     214                 * Return the appropriate translated string for the edit shortcut button.
     215                 *
     216                 * @since 4.7
     217                 *
     218                 * @return {string} Tooltip for edit shortcut.
     219                 */
     220                getEditShortcutTitle: function() {
     221                        var partial = this, l10n = self.data.l10n;
     222                        switch ( partial.getType() ) {
     223                                case 'widget':
     224                                        return l10n.clickEditWidget;
     225                                case 'blogname':
     226                                        return l10n.clickEditTitle;
     227                                case 'blogdescription':
     228                                        return l10n.clickEditTitle;
     229                                case 'nav_menu':
     230                                        return l10n.clickEditMenu;
     231                                default:
     232                                        return l10n.clickEditMisc;
     233                        }
     234                },
     235
     236                /**
     237                 * Return the type of this partial
     238                 *
     239                 * Will use `params.type` if set, but otherwise will try to infer type from settingId.
     240                 *
     241                 * @since 4.7
     242                 *
     243                 * @return {string} Type of partial derived from type param or the related setting ID.
     244                 */
     245                getType: function() {
     246                        var partial = this, settingId;
     247                        settingId = partial.params.primarySetting || _.first( partial.settings() ) || 'unknown';
     248                        if ( partial.params.type ) {
     249                                return partial.params.type;
     250                        }
     251                        if ( settingId.match( /^nav_menu_instance\[/ ) ) {
     252                                return 'nav_menu';
     253                        }
     254                        if ( settingId.match( /^widget_.+\[\d+]$/ ) ) {
     255                                return 'widget';
     256                        }
     257                        return settingId;
     258                },
     259
     260                /**
     261                 * Create an edit shortcut button for this partial.
     262                 *
     263                 * @since 4.7
     264                 *
     265                 * @return {jQuery} The edit shortcut button element.
     266                 */
     267                createEditShortcut: function() {
     268                        var partial = this, shortcutTitle;
     269                        shortcutTitle = partial.getEditShortcutTitle();
     270                        return $( '<button>', {
     271                                'aria-label': shortcutTitle,
     272                                'title': shortcutTitle,
     273                                'type': 'button',
     274                                'class': 'customize-partial-edit-shortcut ' + partial.getEditShortcutClassName()
     275                        } );
     276                },
     277
     278                /**
    102279                 * Find all placements for this partial int he document.
    103280                 *
     
    176353                 */
    177354                showControl: function() {
    178                         var partial = this, settingId = partial.params.primarySetting;
     355                        var partial = this, settingId = partial.params.primarySetting, menuSlug;
    179356                        if ( ! settingId ) {
    180357                                settingId = _.first( partial.settings() );
     358                        }
     359                        if ( partial.getType() === 'nav_menu' ) {
     360                                menuSlug = partial.params.navMenuArgs.theme_location;
     361                                if ( menuSlug ) {
     362                                        settingId = 'nav_menu_locations[' + menuSlug + ']';
     363                                }
    181364                        }
    182365                        api.preview.send( 'focus-control-for-setting', settingId );
     
    320503                        /* jshint ignore:end */
    321504
     505                        partial.createEditShortcutForPlacement( placement );
    322506                        placement.container.removeClass( 'customize-partial-refreshing' );
    323507
     
    8551039                } );
    8561040
     1041                api.preview.bind( 'edit-shortcut-visibility', function( visibility ) {
     1042                        api.selectiveRefresh.editShortcutVisibility.set( visibility );
     1043                } );
     1044                api.selectiveRefresh.editShortcutVisibility.bind( function( visibility ) {
     1045                        var body = $( document.body ), shouldAnimateHide;
     1046
     1047                        shouldAnimateHide = ( 'hidden' === visibility && body.hasClass( 'customize-partial-edit-shortcuts-shown' ) && ! body.hasClass( 'customize-partial-edit-shortcuts-hidden' ) );
     1048                        body.toggleClass( 'customize-partial-edit-shortcuts-hidden', shouldAnimateHide );
     1049                        body.toggleClass( 'customize-partial-edit-shortcuts-shown', 'visible' === visibility );
     1050                        body.toggleClass( 'customize-partial-edit-shortcuts-flash', 'initial' === visibility );
     1051                } );
     1052
    8571053                api.preview.bind( 'active', function() {
     1054                        var body = $( document.body ), buttonText, $editShortcutVisibilityButton;
     1055
     1056                        // Add invisible button to toggle editShortcutVisibility.
     1057                        if ( $( '.edit-shortcut-visibility-button' ).length < 1 ) {
     1058                                buttonText = self.data.l10n.editShortcutVisibilityToggle || 'Toggle edit shortcuts';
     1059                                $editShortcutVisibilityButton = $( '<button type="button" class="edit-shortcut-visibility-button screen-reader-text"></button>' );
     1060                                $editShortcutVisibilityButton.text( buttonText );
     1061                                $editShortcutVisibilityButton.on( 'click', function() {
     1062                                        api.selectiveRefresh.editShortcutVisibility.set( 'visible' === api.selectiveRefresh.editShortcutVisibility.get() ? 'hidden' : 'visible' );
     1063                                } );
     1064                                body.prepend( $editShortcutVisibilityButton );
     1065                        }
    8581066
    8591067                        // Make all partials ready.
     
    8661074                                partial.deferred.ready.resolve();
    8671075                        } );
     1076
     1077                        body.on( 'click', function( event ) {
     1078                                if ( $( event.target ).is( '.customize-partial-edit-shortcut, :input, a[href]' ) || 0 !== $( event.target ).closest( 'a' ).length ) {
     1079                                        return; // Don't toggle shortcuts on form, link, or link child clicks.
     1080                                }
     1081                                api.selectiveRefresh.editShortcutVisibility.set( 'visible' === api.selectiveRefresh.editShortcutVisibility.get() ? 'hidden' : 'visible' );
     1082                                api.preview.send( 'edit-shortcut-visibility', api.selectiveRefresh.editShortcutVisibility.get() );
     1083                        } );
    8681084                } );
    8691085
  • trunk/src/wp-includes/script-loader.php

    r38880 r38967  
    857857        $styles->add( 'media-views',          "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
    858858        $styles->add( 'wp-pointer',           "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
    859         $styles->add( 'customize-preview',    "/wp-includes/css/customize-preview$suffix.css" );
     859        $styles->add( 'customize-preview',    "/wp-includes/css/customize-preview$suffix.css", array( 'dashicons' ) );
    860860        $styles->add( 'wp-embed-template-ie', "/wp-includes/css/wp-embed-template-ie$suffix.css" );
    861861        $styles->add_data( 'wp-embed-template-ie', 'conditional', 'lte IE 8' );
Note: See TracChangeset for help on using the changeset viewer.