Make WordPress Core

Changeset 38967


Ignore:
Timestamp:
10/26/2016 08:02:51 PM (7 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.