From fda4409f409d42a1eca7a4e84c3c356764d4229a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 26 Oct 2016 20:02:51 +0000 Subject: [PATCH] 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. git-svn-id: https://develop.svn.wordpress.org/trunk@38967 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/customize.php | 2 +- src/wp-admin/js/customize-controls.js | 11 + .../themes/twentyfourteen/style.css | 4 + src/wp-includes/css/customize-preview.css | 150 ++++++++++++ .../class-wp-customize-selective-refresh.php | 5 + .../js/customize-preview-widgets.js | 3 +- .../js/customize-selective-refresh.js | 224 +++++++++++++++++- src/wp-includes/script-loader.php | 2 +- 8 files changed, 394 insertions(+), 7 deletions(-) diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index 2f796cb4c9..89d385d7bf 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -161,7 +161,7 @@ do_action( 'customize_controls_print_scripts' );
diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js index 886038eb93..f28df6e873 100644 --- a/src/wp-admin/js/customize-controls.js +++ b/src/wp-admin/js/customize-controls.js @@ -4302,6 +4302,7 @@ if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) { synced.scroll = previewer.scroll; } + synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get(); previewer.send( 'sync', synced ); // Set the previewUrl without causing the url to set the iframe. @@ -5122,6 +5123,7 @@ expandedSection = state.create( 'expandedSection' ), changesetStatus = state.create( 'changesetStatus' ), previewerAlive = state.create( 'previewerAlive' ), + editShortcutVisibility = state.create( 'editShortcutVisibility' ), populateChangesetUuidParam; state.bind( 'change', function() { @@ -5158,6 +5160,7 @@ expandedPanel( false ); expandedSection( false ); previewerAlive( true ); + editShortcutVisibility( 'initial' ); changesetStatus( api.settings.changeset.status ); api.bind( 'change', function() { @@ -5751,6 +5754,14 @@ api.previewer.refresh(); }); + // Update the edit shortcut visibility state. + api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) { + api.state( 'editShortcutVisibility' ).set( visibility ); + } ); + api.state( 'editShortcutVisibility' ).bind( function( visibility ) { + api.previewer.send( 'edit-shortcut-visibility', visibility ); + } ); + // Autosave changeset. ( function() { var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false; diff --git a/src/wp-content/themes/twentyfourteen/style.css b/src/wp-content/themes/twentyfourteen/style.css index 4221371a92..bf5ec111d7 100644 --- a/src/wp-content/themes/twentyfourteen/style.css +++ b/src/wp-content/themes/twentyfourteen/style.css @@ -1039,6 +1039,10 @@ span + .edit-link:before, outline: 1px dotted; } +.secondary-navigation .customize-partial-edit-shortcut:before, +.footer-sidebar .widget:first-child .customize-partial-edit-shortcut:before { + left: 0; +} /** * 6.0 Content diff --git a/src/wp-includes/css/customize-preview.css b/src/wp-includes/css/customize-preview.css index 75251ea93a..7ce4fce8c3 100644 --- a/src/wp-includes/css/customize-preview.css +++ b/src/wp-includes/css/customize-preview.css @@ -10,3 +10,153 @@ -webkit-box-shadow: none; box-shadow: none; } + +/* Make shortcut buttons essentially invisible */ +.widget button.customize-partial-edit-shortcut, +.customize-partial-edit-shortcut { + position: relative; + float: left; + width: 1px; /* required to have a size to be focusable in Safari */ + height: 1px; + padding: 0; + margin: -1px 0 0 -1px; + border: 0; + background: transparent; + color: transparent; + box-shadow: none; + outline: none; + z-index: 5; +} + +.customize-partial-edit-shortcut:active { + padding: 0; + border: 0; +} + +/* Styles for the actual shortcut */ +.customize-partial-edit-shortcut:before { + position: absolute; + left: -36px; + color: #fff; + width: 30px; + height: 30px; + font-size: 18px; + font-family: dashicons; + content: '\f464'; + z-index: 5; + background-color: #0085ba; + border-radius: 50%; + border: 2px solid #fff; + box-shadow: 0 2px 1px rgba(46,68,83,0.15); + text-align: center; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + cursor: pointer; + padding: 0; + animation-fill-mode: both; + animation-duration: .4s; + opacity: 0; + pointer-events: none; + text-shadow: 0 -1px 1px #006799, + 1px 0 1px #006799, + 0 1px 1px #006799, + -1px 0 1px #006799; +} + +.customize-partial-edit-shortcut:hover:before, +.customize-partial-edit-shortcut:focus:before { + background: #008ec2; /* matches primary buttons */ + border-color: #191e23; /* provides visual focus style with 4.5 contrast against background color */ +} + +body.customize-partial-edit-shortcuts-shown .customize-partial-edit-shortcut:before { + animation-name: customize-partial-edit-shortcut-bounce-appear; + pointer-events: auto; +} +body.customize-partial-edit-shortcuts-hidden .customize-partial-edit-shortcut:before { + animation-name: customize-partial-edit-shortcut-bounce-disappear; + pointer-events: none; +} + +body.customize-partial-edit-shortcuts-flash .customize-partial-edit-shortcut:before { + animation-duration: 1.5s; + animation-delay: 0.4s; + animation-name: customize-partial-edit-shortcut-bounce-disappear; + pointer-events: none; +} + +.page-sidebar-collapsed .customize-partial-edit-shortcut:before, +.customize-partial-edit-shortcut-hidden:before { + visibility: hidden; +} + +.widget button.customize-partial-edit-shortcut-absolute, +.customize-partial-edit-shortcut-absolute { + position: static; +} + +.customize-partial-edit-shortcut-left-margin:before { + left: 0; +} + +@keyframes customize-partial-edit-shortcut-bounce-appear { + from, 20%, 40%, 60%, 80%, to { + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + 0% { + opacity: 0; + transform: scale3d(.3, .3, .3); + } + 20% { + transform: scale3d(1.1, 1.1, 1.1); + } + 40% { + transform: scale3d(.9, .9, .9); + } + 60% { + opacity: 1; + transform: scale3d(1.03, 1.03, 1.03); + } + 80% { + transform: scale3d(.97, .97, .97); + } + to { + opacity: 1; + transform: scale3d(1, 1, 1); + } +} + +@keyframes customize-partial-edit-shortcut-bounce-disappear { + from, 20%, 40%, 60%, 80%, to { + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + 0% { + opacity: 1; + transform: scale3d(1, 1, 1); + } + 20% { + transform: scale3d(.97, .97, .97); + } + 40% { + opacity: 1; + transform: scale3d(1.03, 1.03, 1.03); + } + 60% { + transform: scale3d(.9, .9, .9); + } + 80% { + transform: scale3d(1.1, 1.1, 1.1); + } + to { + opacity: 0; + transform: scale3d(.3, .3, .3); + } +} + +@media screen and (max-width:800px) { + .customize-partial-edit-shortcut:before { + left: -18px; /* Assume there will be less of a margin available on smaller screens */ + } +} diff --git a/src/wp-includes/customize/class-wp-customize-selective-refresh.php b/src/wp-includes/customize/class-wp-customize-selective-refresh.php index 6864eba63d..73af9c9e23 100644 --- a/src/wp-includes/customize/class-wp-customize-selective-refresh.php +++ b/src/wp-includes/customize/class-wp-customize-selective-refresh.php @@ -184,6 +184,11 @@ final class WP_Customize_Selective_Refresh { 'renderQueryVar' => self::RENDER_QUERY_VAR, 'l10n' => array( 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ), + 'clickEditMenu' => __( 'Click to edit this menu.' ), + 'clickEditWidget' => __( 'Click to edit this widget.' ), + 'clickEditTitle' => __( 'Click to edit the site title.' ), + 'clickEditMisc' => __( 'Click to edit this element.' ), + 'editShortcutVisibilityToggle' => __( 'Toggle edit shortcuts' ), /* translators: %s: document.write() */ 'badDocumentWrite' => sprintf( __( '%s is forbidden' ), 'document.write()' ), ), diff --git a/src/wp-includes/js/customize-preview-widgets.js b/src/wp-includes/js/customize-preview-widgets.js index 967d56af8e..6c05e89431 100644 --- a/src/wp-includes/js/customize-preview-widgets.js +++ b/src/wp-includes/js/customize-preview-widgets.js @@ -357,7 +357,6 @@ wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( widgetPartial = new self.WidgetPartial( partialId, { params: {} } ); - api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial ); } // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder. @@ -400,6 +399,8 @@ wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( wasInserted = true; } ); + api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial ); + if ( wasInserted ) { sidebarPartial.reflowWidgets(); } diff --git a/src/wp-includes/js/customize-selective-refresh.js b/src/wp-includes/js/customize-selective-refresh.js index 427a7c3c9b..953105775d 100644 --- a/src/wp-includes/js/customize-selective-refresh.js +++ b/src/wp-includes/js/customize-selective-refresh.js @@ -6,6 +6,7 @@ wp.customize.selectiveRefresh = ( function( $, api ) { self = { ready: $.Deferred(), + editShortcutVisibility: new api.Value(), data: { partials: {}, renderQueryVar: '', @@ -42,7 +43,7 @@ wp.customize.selectiveRefresh = ( function( $, api ) { id: null, - /** + /** * Constructor. * * @since 4.5.0 @@ -82,8 +83,9 @@ wp.customize.selectiveRefresh = ( function( $, api ) { */ ready: function() { var partial = this; - _.each( _.pluck( partial.placements(), 'container' ), function( container ) { - $( container ).attr( 'title', self.data.l10n.shiftClickToEdit ); + _.each( partial.placements(), function( placement ) { + $( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit ); + partial.createEditShortcutForPlacement( placement ); } ); $( document ).on( 'click', partial.params.selector, function( e ) { if ( ! e.shiftKey ) { @@ -98,6 +100,181 @@ wp.customize.selectiveRefresh = ( function( $, api ) { } ); }, + /** + * Create and show the edit shortcut for a given partial placement container. + * + * @since 4.7 + * + * @param {Placement} placement The placement container element. + * @returns {void} + */ + createEditShortcutForPlacement: function( placement ) { + var partial = this, $shortcut, $placementContainer; + if ( ! placement.container ) { + return; + } + $placementContainer = $( placement.container ); + if ( ! $placementContainer.length ) { + return; + } + $shortcut = partial.createEditShortcut(); + partial.positionEditShortcut( placement, $shortcut ); + $shortcut.on( 'click', function( event ) { + event.preventDefault(); + event.stopPropagation(); + partial.showControl(); + } ); + }, + + /** + * Position an edit shortcut for the placement container. + * + * The shortcut must already be created and added to the page. + * + * @since 4.7 + * + * @param {Placement} placement The placement for the partial. + * @param {jQuery} $editShortcut The shortcut element as a jQuery object. + * @returns {void} + */ + positionEditShortcut: function( placement, $editShortcut ) { + var partial = this, $placementContainer; + $placementContainer = $( placement.container ); + $placementContainer.prepend( $editShortcut ); + if ( 'absolute' === $placementContainer.css( 'position' ) ) { + $editShortcut.addClass( 'customize-partial-edit-shortcut-absolute' ); + $editShortcut.css( partial.getEditShortcutPositionStyles( $placementContainer ) ); + partial.whenPageChanges( function() { + $editShortcut.css( partial.getEditShortcutPositionStyles( $placementContainer ) ); + } ); + } + if ( ! $placementContainer.is( ':visible' ) || 'none' === $placementContainer.css( 'display' ) ) { + $editShortcut.addClass( 'customize-partial-edit-shortcut-hidden' ); + } + $editShortcut.toggleClass( 'customize-partial-edit-shortcut-left-margin', $editShortcut.offset().left < 1 ); + }, + + /** + * Call a callback function when the page changes. + * + * This calls a callback for any change that might require refreshing the edit shortcuts. + * + * @since 4.7 + * + * @param {Function} callback The function to call when the page changes. + * @returns {void} + */ + whenPageChanges: function( callback ) { + var debouncedCallback, $document; + debouncedCallback = _.debounce( function() { + // Timeout allows any page animations to finish + setTimeout( callback, 100 ); + }, 350 ); + // When window is resized. + $( window ).resize( debouncedCallback ); + // When any customizer setting changes. + api.bind( 'change', debouncedCallback ); + $document = $( window.document ); + // After scroll in case there are fixed position elements + $document.on( 'scroll', debouncedCallback ); + // After page click (eg: hamburger menus) + $document.on( 'click', debouncedCallback ); + }, + + /** + * Return the CSS positioning for the edit shortcut for a given partial placement. + * + * @since 4.7 + * + * @param {jQuery} $placementContainer The placement container element as a jQuery object. + * @return {Object} Object containing CSS positions. + */ + getEditShortcutPositionStyles: function( $placementContainer ) { + return { + top: $placementContainer.css( 'top' ), + left: $placementContainer.css( 'left' ), + right: 'auto' + }; + }, + + /** + * Return the unique class name for the edit shortcut button for this partial. + * + * @since 4.7 + * + * @return {string} Partial ID converted into a class name for use in shortcut. + */ + getEditShortcutClassName: function() { + var partial = this, cleanId; + cleanId = partial.id.replace( /]/g, '' ).replace( /\[/g, '-' ); + return 'customize-partial-edit-shortcut-' + cleanId; + }, + + /** + * Return the appropriate translated string for the edit shortcut button. + * + * @since 4.7 + * + * @return {string} Tooltip for edit shortcut. + */ + getEditShortcutTitle: function() { + var partial = this, l10n = self.data.l10n; + switch ( partial.getType() ) { + case 'widget': + return l10n.clickEditWidget; + case 'blogname': + return l10n.clickEditTitle; + case 'blogdescription': + return l10n.clickEditTitle; + case 'nav_menu': + return l10n.clickEditMenu; + default: + return l10n.clickEditMisc; + } + }, + + /** + * Return the type of this partial + * + * Will use `params.type` if set, but otherwise will try to infer type from settingId. + * + * @since 4.7 + * + * @return {string} Type of partial derived from type param or the related setting ID. + */ + getType: function() { + var partial = this, settingId; + settingId = partial.params.primarySetting || _.first( partial.settings() ) || 'unknown'; + if ( partial.params.type ) { + return partial.params.type; + } + if ( settingId.match( /^nav_menu_instance\[/ ) ) { + return 'nav_menu'; + } + if ( settingId.match( /^widget_.+\[\d+]$/ ) ) { + return 'widget'; + } + return settingId; + }, + + /** + * Create an edit shortcut button for this partial. + * + * @since 4.7 + * + * @return {jQuery} The edit shortcut button element. + */ + createEditShortcut: function() { + var partial = this, shortcutTitle; + shortcutTitle = partial.getEditShortcutTitle(); + return $( '' ); + $editShortcutVisibilityButton.text( buttonText ); + $editShortcutVisibilityButton.on( 'click', function() { + api.selectiveRefresh.editShortcutVisibility.set( 'visible' === api.selectiveRefresh.editShortcutVisibility.get() ? 'hidden' : 'visible' ); + } ); + body.prepend( $editShortcutVisibilityButton ); + } // Make all partials ready. self.partial.each( function( partial ) { @@ -865,6 +1073,14 @@ wp.customize.selectiveRefresh = ( function( $, api ) { self.partial.bind( 'add', function( partial ) { partial.deferred.ready.resolve(); } ); + + body.on( 'click', function( event ) { + if ( $( event.target ).is( '.customize-partial-edit-shortcut, :input, a[href]' ) || 0 !== $( event.target ).closest( 'a' ).length ) { + return; // Don't toggle shortcuts on form, link, or link child clicks. + } + api.selectiveRefresh.editShortcutVisibility.set( 'visible' === api.selectiveRefresh.editShortcutVisibility.get() ? 'hidden' : 'visible' ); + api.preview.send( 'edit-shortcut-visibility', api.selectiveRefresh.editShortcutVisibility.get() ); + } ); } ); } ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 143bfa14ff..89bcc063c0 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -856,7 +856,7 @@ function wp_default_styles( &$styles ) { $styles->add( 'editor-buttons', "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) ); $styles->add( 'media-views', "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) ); $styles->add( 'wp-pointer', "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) ); - $styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css" ); + $styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css", array( 'dashicons' ) ); $styles->add( 'wp-embed-template-ie', "/wp-includes/css/wp-embed-template-ie$suffix.css" ); $styles->add_data( 'wp-embed-template-ie', 'conditional', 'lte IE 8' );