diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index 6c81fa8247..45c49b5fa5 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -24,11 +24,94 @@ body { color: #555d66; } -#customize-header-actions .button-primary { +#customize-save-button-wrapper { float: right; margin-top: 9px; } +#customize-save-button-wrapper .save { + float: left; + border-radius: 3px; + box-shadow: none; /* @todo Adjust box shadow based on the disable states of paired button. */ + display: none; /* Shown when ready. */ + margin-top: 0; +} +#customize-save-button-wrapper .save.has-next-sibling { + border-radius: 3px 0 0 3px; +} + +#customize-outer-theme-controls-wrapper { + position: absolute; + top: 0; + bottom: 0; + left: -301px; + visibility: hidden; + overflow-x: hidden; + overflow-y: auto; + width: 300px; + margin: 0; + z-index: 4; + background: #eee; + transition: left .18s; + border-right: 1px solid #ddd; +} + +.outer-section-open .wp-full-overlay.expanded { + margin-left: 300px; +} + +#customize-theme-controls .control-section-outer { + display: none !important; +} + +#customize-outer-theme-controls .accordion-section-content { + padding: 12px; +} + +#customize-outer-theme-controls .accordion-section-content.open { + display: block; +} + +.outer-section-open .wp-full-overlay.expanded #customize-outer-theme-controls-wrapper { + visibility: visible; + left: 0; + transition: left .18s; +} + +.customize-outer-pane-parent { + margin: 0; +} + +.outer-section-open .wp-full-overlay.expanded #customize-preview { + opacity: 0.4; +} + +body.outer-section-open .wp-full-overlay.expanded .wp-full-overlay-main { + left: 300px; +} + +#customize-outer-theme-controls li.notice { + padding-top: 8px; + padding-bottom: 8px; + margin-left: 0; + margin-bottom: 10px; +} + +#publish-settings { + text-indent: 0; + border-radius: 0 3px 3px 0; + padding-left: 0; + padding-right: 0; + box-shadow: none; /* @todo Adjust box shadow based on the disable states of paired button. */ + font-size: 14px; + width: 30px; + float: left; + display: none; /* Shown when ready. */ + -webkit-transform: none; + transform: none; + margin-top: 0; +} + #customize-header-actions .spinner { margin-top: 13px; margin-right: 4px; @@ -53,10 +136,181 @@ body { margin-bottom: 15px; } +#customize-control-changeset_status label, +#customize-control-changeset_preview_link input { + background-color: #ffffff; + border-bottom: 1px solid #ddd; + box-sizing: content-box; + width: 100%; + margin-left: -12px; + padding-left: 12px; + padding-right: 12px; +} + +#customize-controls .date-input:invalid { + border-color: red; +} + +.date-time-fields .month-field { + width: 79px; +} + +.date-time-fields .day-field, +.date-time-fields .hour-field, +.date-time-fields .minute-field { + width: 46px; +} + +.date-time-fields .year-field { + width: 60px; +} + +.date-time-fields .am-pm-field { + width: 53px; +} + +#customize-control-changeset_status label { + padding-top: 10px; + padding-bottom: 10px; + font-weight: 500; +} + +#customize-control-changeset_status label:first-of-type { + border-top: 1px solid #ddd; +} + +#customize-control-changeset_status .customize-control-title { + margin-bottom: 6px; +} + +#customize-control-changeset_status input { + margin-left: 0; +} + +#customize-control-changeset_preview_link { + position: relative; + display: block; +} + +.customize-copy-preview-link { + position: absolute; + bottom: 9px; + right: 0; +} + +.customize-copy-preview-link:before, +.customize-copy-preview-link:after { + content: ''; + height: 28px; + position: absolute; + background: #ffffff; + top: -1px; +} + +.customize-copy-preview-link:before { + left: -10px; + width: 9px; + opacity: 0.75; +} + +.customize-copy-preview-link:after { + left: -5px; + width: 4px; + opacity: 0.8; +} + +#customize-control-changeset_preview_link input { + line-height: 2.5; + border-top: 1px solid #ddd; + border-left: none; + border-right: none; + text-indent: -999px; + color: white; +} + +#customize-control-changeset_preview_link label { + position: relative; + display: block; +} + +#customize-control-changeset_preview_link a.preview-control-element { + display: inline-block; + position: absolute; + white-space: nowrap; + overflow: hidden; + width: 217px; + bottom: 14px; + font-size: 14px; + text-decoration: none; +} + +#customize-control-changeset_preview_link a.preview-control-element.disabled, +#customize-control-changeset_preview_link a.preview-control-element.disabled:active, +#customize-control-changeset_preview_link a.preview-control-element.disabled:focus, +#customize-control-changeset_preview_link a.preview-control-element.disabled:visited { + color: black; + opacity: 0.4; + cursor: default; + outline: none; + box-shadow: none; +} + +#sub-accordion-section-publish_settings .customize-section-description-container { + display: none; +} + #customize-controls .customize-info.section-meta { margin-bottom: 15px; } +.date-time-fields { + padding-top: 10px; + padding-bottom:10px; +} + +.date-time-fields label, +.date-time-fields .date-time-separator { + float: left; + margin-right:5px; +} + +.date-time-fields .date-time-separator { + line-height: 2; +} + +.date-time-fields .time-row { + padding-top: 12px; +} + +.date-time-fields .date-timezone { + float: left; + line-height: 2.2; + text-decoration: none; +} + +#customize-control-changeset_preview_link { + margin-top: 20px; +} + +#customize-control-changeset_status { + margin-bottom: 0; + padding-bottom: 0; +} + +#customize-control-changeset_scheduled_date { + box-sizing: content-box; + width: 100%; + margin-left: -12px; + padding: 12px 12px 18px; + background: #ffffff; + border-bottom: 1px solid #ddd; + margin-bottom: 0; +} + +#customize-control-changeset_scheduled_date .customize-control-description { + font-style: normal; +} + #customize-controls .customize-info.is-in-view, #customize-controls .customize-section-title.is-in-view { position: absolute; @@ -105,6 +359,8 @@ body { #customize-controls .customize-pane-child .customize-section-title h3, #customize-controls .customize-pane-child h3.customize-section-title, +#customize-outer-theme-controls .customize-pane-child .customize-section-title h3, +#customize-outer-theme-controls .customize-pane-child h3.customize-section-title, #customize-controls .customize-info .panel-title { font-size: 20px; font-weight: 200; @@ -150,6 +406,7 @@ body { #customize-controls .customize-info .customize-panel-description, #customize-controls .customize-info .customize-section-description, +#customize-outer-theme-controls .customize-info .customize-section-description, #customize-controls .no-widget-areas-rendered-notice { color: #555d66; display: none; @@ -171,7 +428,8 @@ body { margin-bottom: 0; } -#customize-controls .customize-info .customize-section-description { +#customize-controls .customize-info .customize-section-description, +#customize-outer-theme-controls .customize-section-description { margin-bottom: 15px; } @@ -189,11 +447,13 @@ body { padding-right: 30px; } -#customize-theme-controls .control-section { +#customize-theme-controls .control-section, +#customize-outer-theme-controls .control-section { border: none; } -#customize-theme-controls .accordion-section-title { +#customize-theme-controls .accordion-section-title, +#customize-outer-theme-controls .accordion-section-title { color: #555d66; background-color: #fff; border-bottom: 1px solid #ddd; @@ -209,12 +469,14 @@ body { border-left: 4px solid #fff; } -#customize-theme-controls .accordion-section-title:after { +#customize-theme-controls .accordion-section-title:after, +#customize-outer-theme-controls .accordion-section-title:after { content: "\f345"; color: #a0a5aa; } -#customize-theme-controls .accordion-section-content { +#customize-theme-controls .accordion-section-content, +#customize-outer-theme-controls .accordion-section-content { color: #555d66; background: transparent; } @@ -222,6 +484,9 @@ body { #customize-controls .control-section:hover > .accordion-section-title, #customize-controls .control-section .accordion-section-title:hover, #customize-controls .control-section.open .accordion-section-title, +#customize-outer-theme-controls .control-section .accordion-section-title:hover, +#customize-outer-theme-controls .control-section.open .accordion-section-title, +#customize-outer-theme-controls .control-section .accordion-section-title:focus, #customize-controls .control-section .accordion-section-title:focus { color: #0073aa; background: #f3f3f5; @@ -242,7 +507,11 @@ body { #customize-theme-controls .control-section:hover > .accordion-section-title:after, #customize-theme-controls .control-section .accordion-section-title:hover:after, #customize-theme-controls .control-section.open .accordion-section-title:after, -#customize-theme-controls .control-section .accordion-section-title:focus:after { +#customize-theme-controls .control-section .accordion-section-title:focus:after, +#customize-outer-theme-controls .control-section:hover > .accordion-section-title:after, +#customize-outer-theme-controls .control-section .accordion-section-title:hover:after, +#customize-outer-theme-controls .control-section.open .accordion-section-title:after, +#customize-outer-theme-controls .control-section .accordion-section-title:focus:after { color: #0073aa; } @@ -250,7 +519,8 @@ body { border-bottom: 1px solid #eee; } -#customize-theme-controls .control-section.open .accordion-section-title { +#customize-theme-controls .control-section.open .accordion-section-title, +#customize-outer-theme-controls .control-section.open .accordion-section-title { border-bottom-color: #eee !important; } @@ -828,6 +1098,10 @@ p.customize-section-description { margin: 0; } +.wp-full-overlay.collapsed #customize-controls #customize-notifications-area { + display: none !important; +} + #customize-controls #customize-notifications-area, #customize-controls .customize-section-title > .customize-control-notifications-container, #customize-controls .panel-meta > .customize-control-notifications-container { @@ -1119,18 +1393,60 @@ p.customize-section-description { animation: dice-color-change 3s infinite; } -@-webkit-keyframes dice-color-change { - 0% { color: #d4b146; } - 50% { color: #ef54b0; } - 75% { color: #7190d3; } - 100% { color: #d4b146; } +.button-see-me { + -webkit-animation: bounce .7s 1; + animation: bounce .7s 1; + -webkit-transform-origin: center bottom; + transform-origin: center bottom; } -@keyframes dice-color-change { - 0% { color: #d4b146; } - 50% { color: #ef54b0; } - 75% { color: #7190d3; } - 100% { color: #d4b146; } +@-webkit-keyframes bounce { + from, 20%, 53%, 80%, to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + -webkit-transform: translate3d(0,0,0); + } + + 40%, 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -12px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -6px, 0); + } + + 90% { + -webkit-transform: translate3d(0,-1px,0); + } +} + +@keyframes bounce { + from, 20%, 53%, 80%, to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + -webkit-transform: translate3d(0,0,0); + transform: translate3d(0,0,0); + } + + 40%, 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -12px, 0); + transform: translate3d(0, -12px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -6px, 0); + transform: translate3d(0, -6px, 0); + } + + 90% { + -webkit-transform: translate3d(0,-1px,0); + transform: translate3d(0,-1px,0); + } } .customize-control-header .choice { @@ -1310,7 +1626,8 @@ p.customize-section-description { } #customize-controls .control-section-themes .accordion-section-title span.customize-action, -#customize-controls .customize-section-title span.customize-action { +#customize-controls .customize-section-title span.customize-action, +#customize-outer-theme-controls .customize-section-title span.customize-action { font-size: 13px; display: block; font-weight: 400; @@ -1843,6 +2160,27 @@ body.adding-widget .add-new-widget:before, line-height: 32px; } + .customize-control .date-time-fields select { + height: 39px; + } + + .date-time-fields .month-field { + width: 79px; + } + + .date-time-fields .day-field, + .date-time-fields .hour-field, + .date-time-fields .minute-field { + width: 55px; + } + + .date-time-fields .year-field { + width: 80px; + } + + .date-time-fields .date-timezone { + line-height: 3.2; + } .wp-core-ui.wp-customizer .button { margin-top: 12px; } @@ -1853,7 +2191,8 @@ body.adding-widget .add-new-widget:before, width: 100%; } - .wp-full-overlay.expanded { + .wp-full-overlay.expanded, + .outer-section-open .wp-full-overlay.expanded { margin-left: 0; } @@ -1931,12 +2270,17 @@ body.adding-widget .add-new-widget:before, margin-top: 12px; } - #customize-header-actions .button-primary { - margin-top: 6px; + #publish-settings { + height: 31px; + } + + #customize-control-changeset_status label { + padding-top: 15px; } body.adding-widget div#available-widgets, - body.adding-menu-items div#available-menu-items { + body.adding-menu-items div#available-menu-items, + body.outer-section-open div#customize-outer-theme-controls-wrapper { top: 46px; left: 0; z-index: 10; diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index 81a7ae2741..3fc2b5b6bd 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -27,14 +27,30 @@ if ( ! current_user_can( 'customize' ) ) { global $wp_scripts, $wp_customize; if ( $wp_customize->changeset_post_id() ) { - if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) { + $changeset_post = get_post( $wp_customize->changeset_post_id() ); + + if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post->ID ) ) { wp_die( '

' . __( 'Cheatin’ uh?' ) . '

' . '

' . __( 'Sorry, you are not allowed to edit this changeset.' ) . '

', 403 ); } - if ( in_array( get_post_status( $wp_customize->changeset_post_id() ), array( 'publish', 'trash' ), true ) ) { + + $missed_schedule = ( + 'future' === $changeset_post->post_status && + get_post_time( 'G', true, $changeset_post ) < time() + ); + if ( $missed_schedule ) { + wp_publish_post( $changeset_post->ID ); + wp_die( + '

' . __( 'Your scheduled changes just published' ) . '

' . + '

' . __( 'Customize New Changes' ) . '

', + 200 + ); + } + + if ( in_array( get_post_status( $changeset_post->ID ), array( 'publish', 'trash' ), true ) ) { wp_die( '

' . __( 'Cheatin’ uh?' ) . '

' . '

' . __( 'This changeset has already been published and cannot be further modified.' ) . '

' . @@ -132,14 +148,11 @@ do_action( 'customize_controls_print_scripts' );
- is_theme_active() ? __( 'Save & Publish' ) : __( 'Save & Activate' ); - $save_attrs = array(); - if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) { - $save_attrs['style'] = 'display: none'; - } - submit_button( $save_text, 'primary save', 'save', false, $save_attrs ); - ?> + is_theme_active() ? __( 'Publish' ) : __( 'Activate & Publish' ); ?> +
+ + +
+
+
+
+
+
+
+
= midDayHour ? 'pm' : 'am'; + date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour ); + delete date.second; + } + + return date; + }, + + /** + * Validates if input components have valid date and time. + * + * @since 4.9.0 + * @return {boolean} If date input fields has error. + */ + validateInputs: function validateInputs() { + var control = this, errorMessage; + + control.invalidDate = false; + + _.each( [ 'day', 'hour', 'year', 'minute' ], function( component ) { + var element, el, max, min, maxLength, value; + + if ( ! control.invalidDate ) { + element = control.inputElements[ component ]; + el = element.element.get( 0 ); + max = parseInt( element.element.attr( 'max' ), 10 ); + min = parseInt( element.element.attr( 'min' ), 10 ); + maxLength = parseInt( element.element.attr( 'maxlength' ), 10 ); + value = element(); + control.invalidDate = value > max || value < min || String( value ).length > maxLength; + errorMessage = control.invalidDate ? api.l10n.invalid + ' ' + component : ''; + + el.setCustomValidity( errorMessage ); + _.result( el, 'reportValidity' ); + } + } ); + + return control.invalidDate; + }, + + /** + * Updates number of days according to the month and year selected. + * + * @since 4.9.0 + * @return {void} + */ + updateDaysForMonth: function updateDaysForMonth() { + var control = this, daysInMonth, year, month, day; + + month = control.inputElements.month(); + year = control.inputElements.year(); + day = control.inputElements.day(); + + if ( month && year ) { + daysInMonth = new Date( year, month, 0 ).getDate(); + control.inputElements.day.element.attr( 'max', daysInMonth ); + + if ( day > daysInMonth ) { + control.inputElements.day( daysInMonth ); + } + } + }, + + /** + * Updates number of minutes according to the hour selected. + * + * @since 4.9.0 + * @return {void} + */ + updateMinutesForHour: function updateMinutesForHour() { + var control = this, maxHours = 24, minuteEl; + + if ( control.inputElements.ampm ) { + return; + } + + minuteEl = control.inputElements.minute.element; + + if ( maxHours === control.inputElements.hour() ) { + control.inputElements.minute( 0 ); + minuteEl.data( 'default-max', minuteEl.attr( 'max' ) ); + minuteEl.data( 'default-maxlength', minuteEl.attr( 'maxlength' ) ); + minuteEl.attr( 'max', '0' ); + } else if ( minuteEl.data( 'default-max' ) ) { + minuteEl.attr( 'max', minuteEl.data( 'default-max' ) ); + minuteEl.attr( 'maxlength', minuteEl.data( 'maxlength' ) ); + } + }, + + /** + * Populate setting value from the inputs. + * + * @since 4.9.0 + * @returns {boolean} If setting updated. + */ + populateSetting: function populateSetting() { + var control = this, date; + + if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) { + return false; + } + + date = control.convertInputDateToString(); + control.setting.set( date ); + return true; + }, + + /** + * Converts input values to string in Y-m-d H:i:s format. + * + * @since 4.9.0 + * @return {string} Date string. + */ + convertInputDateToString: function convertInputDateToString() { + var control = this, date = '', dateFormat, hourInTwentyFourHourFormat, + getElementValue, pad; + + pad = function( number, padding ) { + var zeros; + if ( String( number ).length < padding ) { + zeros = padding - String( number ).length; + number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number ); + } + return number; + }; + + getElementValue = function( component ) { + var value = control.inputElements[ component ].get(); + + if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) { + value = pad( value, 2 ); + } else if ( 'year' === component ) { + value = pad( value, 4 ); + } + return value; + }; + + hourInTwentyFourHourFormat = control.inputElements.ampm ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.ampm() ) : control.inputElements.hour(); + dateFormat = [ 'year', '-', 'month', '-', 'day', ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ]; + + _.each( dateFormat, function( component ) { + date += control.inputElements[ component ] ? getElementValue( component ) : component; + } ); + + return date; + }, + + /** + * Check if the date is in the future. + * + * @since 4.9.0 + * @returns {boolean} True if future date. + */ + isFutureDate: function isFutureDate() { + var control = this; + return 0 < api.utils.getRemainingTime( control.convertInputDateToString() ); + }, + + /** + * Convert hour in twelve hour format to twenty four hour format. + * + * @since 4.9.0 + * @param {string} hourInTwelveHourFormat Hour in twelve hour format. + * @param {string} ampm am/pm + * @return {string} Hour in twenty four hour format. + */ + convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, ampm ) { + var hourInTwentyFourHourFormat, hour, midDayHour = 12; + + hour = parseInt( hourInTwelveHourFormat, 10 ); + + if ( 'pm' === ampm && hour < midDayHour ) { + hourInTwentyFourHourFormat = hour + midDayHour; + } else if ( 'am' === ampm && midDayHour === hour ) { + hourInTwentyFourHourFormat = hour - midDayHour; + } else { + hourInTwentyFourHourFormat = hour; + } + + return String( hourInTwentyFourHourFormat ); + }, + + /** + * Populates date inputs in date fields. + * + * @since 4.9.0 + * @returns {boolean} Whether the inputs were populated. + */ + populateDateInputs: function populateDateInputs() { + var control = this, parsed; + + parsed = control.parseDateTime( control.setting.get(), control.params.twelveHourFormat ); + + if ( ! parsed ) { + return false; + } + + _.each( control.inputElements, function( element, component ) { + element.set( parsed[ component ] ); + } ); + + return true; + }, + + /** + * Toggle future date notification for date control. + * + * @since 4.9.0 + * @param {boolean} notify Add or remove the notification. + * @return {wp.customize.DateTimeControl} + */ + toggleFutureDateNotification: function toggleFutureDateNotification( notify ) { + var control = this, notificationCode, notification; + + notificationCode = 'not_future_date'; + + if ( notify ) { + notification = new api.Notification( notificationCode, { + type: 'error', + message: api.l10n.futureDateError + } ); + control.notifications.add( notificationCode, notification ); + } else { + control.notifications.remove( notificationCode ); + } + + return control; + } + }); + + /** + * Class PreviewLinkControl. + * + * @since 4.9.0 + * @constructor + * @augments wp.customize.Control + * @augments wp.customize.Class + */ + api.PreviewLinkControl = api.Control.extend({ + + previewElements: {}, + + /** + * Override the templateSelector before embedding the control into the page. + * + * @since 4.9.0 + * @return {void} + */ + embed: function() { + var control = this; + control.templateSelector = 'customize-preview-link-control'; + return api.Control.prototype.embed.apply( control, arguments ); + }, + + /** + * Initialize behaviors. + * + * @since 4.9.0 + * @returns {void} + */ + ready: function ready() { + var control = this, element, component, node, link, input, button; + + _.bindAll( control, 'updatePreviewLink' ); + + if ( ! control.setting ) { + control.setting = new api.Value(); + } + + control.container.find( '.preview-control-element' ).each( function() { + node = $( this ); + component = node.data( 'component' ); + element = new api.Element( node ); + control.previewElements[ component ] = element; + control.elements.push( element ); + } ); + + link = control.previewElements.link; + input = control.previewElements.input; + button = control.previewElements.button; + + input.link( control.setting ); + link.link( control.setting ); + + link.bind( function( value ) { + link.element.attr( 'href', value ); + link.element.attr( 'target', api.settings.changeset.uuid ); + } ); + + api.bind( 'ready', control.updatePreviewLink ); + api.bind( 'change', control.updatePreviewLink ); + api.state( 'saved' ).bind( control.updatePreviewLink ); + + button.element.on( 'click', function( event ) { + event.preventDefault(); + if ( control.setting() ) { + input.element.select(); + document.execCommand( 'copy' ); + button( button.element.data( 'copied-text' ) ); + } + } ); + + link.element.on( 'click', function( event ) { + if ( link.element.hasClass( 'disabled' ) ) { + event.preventDefault(); + } + } ); + + button.element.on( 'mouseenter', function() { + if ( control.setting() ) { + button( button.element.data( 'copy-text' ) ); + } + } ); + }, + + /** + * Updates Preview Link + * + * @since 4.9.0 + * @return {void} + */ + updatePreviewLink: function updatePreviewLink() { + var control = this, unsavedDirtyValues; + + unsavedDirtyValues = ! _.isEmpty( api.dirtyValues( { + unsaved: true + } ) ); + + control.toggleSaveNotification( unsavedDirtyValues ); + control.previewElements.link.element.toggleClass( 'disabled', unsavedDirtyValues ); + control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues ); + control.setting.set( api.previewer.getFrontendPreviewUrl() ); + }, + + /** + * Toggles save notification. + * + * @since 4.9.0 + * @param {boolean} notify Add or remove notification. + * @return {void} + */ + toggleSaveNotification: function toggleSaveNotification( notify ) { + var control = this, notificationCode, notification; + + notificationCode = 'changes_not_saved'; + + if ( notify ) { + notification = new api.Notification( notificationCode, { + type: 'info', + message: api.l10n.saveBeforeShare + } ); + control.notifications.add( notificationCode, notification ); + } else { + control.notifications.remove( notificationCode ); + } + } + }); + // Change objects contained within the main customize object to Settings. api.defaultConstructor = api.Setting; @@ -4059,7 +4676,7 @@ customize_messenger_channel: previewFrame.query.customize_messenger_channel } ); - if ( ! api.state( 'saved' ).get() ) { + if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) { params.customize_autosaved = 'on'; } @@ -4660,11 +5277,13 @@ background: api.BackgroundControl, background_position: api.BackgroundPositionControl, theme: api.ThemeControl, + date_time: api.DateTimeControl, code_editor: api.CodeEditorControl }; api.panelConstructor = {}; api.sectionConstructor = { - themes: api.ThemesSection + themes: api.ThemesSection, + outer: api.OuterSection }; /** @@ -4836,6 +5455,28 @@ api.trigger( 'pane-contents-reflowed' ); }, api ); + // Define state values. + api.state = new api.Values(); + _.each( [ + 'saved', + 'autosaved', + 'saving', + 'activated', + 'processing', + 'paneVisible', + 'expandedPanel', + 'expandedSection', + 'changesetDate', + 'selectedChangesetDate', + 'changesetStatus', + 'selectedChangesetStatus', + 'remainingTimeToPublish', + 'previewerAlive', + 'editShortcutVisibility' + ], function( name ) { + api.state.create( name ); + }); + $( function() { api.settings = window._wpCustomizeSettings; api.l10n = window._wpCustomizeControlsL10n; @@ -4863,8 +5504,61 @@ title = $( '#customize-info .panel-title.site-title' ), closeBtn = $( '.customize-controls-close' ), saveBtn = $( '#save' ), + btnWrapper = $( '#customize-save-button-wrapper' ), + publishSettingsBtn = $( '#publish-settings' ), footerActions = $( '#customize-footer-actions' ); + saveBtn.show(); + + api.section( 'publish_settings', function( section ) { + var updateButtonsState, previewLinkControl, previewLinkControlId = 'changeset_preview_link'; + + previewLinkControl = new api.PreviewLinkControl( previewLinkControlId, { + params: { + section: section.id, + active: true, + priority: 100, + content: '
  • ' + } + } ); + + api.control.add( previewLinkControlId, previewLinkControl ); + + // Make sure publish settings are not available until the theme has been activated. + if ( ! api.settings.theme.active ) { + section.active.set( false ); + section.active.link( api.state( 'activated' ) ); + } + + // Bind visibility of the publish settings button to whether the section is active. + updateButtonsState = function() { + publishSettingsBtn.toggle( section.active.get() ); + saveBtn.toggleClass( 'has-next-sibling', section.active.get() ); + }; + updateButtonsState(); + section.active.bind( updateButtonsState ); + + section.contentContainer.find( '.customize-action' ).text( api.l10n.updating ); + section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' ); + publishSettingsBtn.prop( 'disabled', false ); + + publishSettingsBtn.on( 'click', function( event ) { + event.preventDefault(); + section.expanded.set( ! section.expanded.get() ); + } ); + + section.expanded.bind( function( isExpanded ) { + publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) ); + publishSettingsBtn.toggleClass( 'active', isExpanded ); + } ); + + api.state( 'changesetStatus' ).bind( function( status ) { + if ( 'publish' === status ) { + section.collapse(); + } + } ); + } ); + // Prevent the form from saving when enter is pressed on an input or select element. $('#customize-controls').on( 'keydown', function( e ) { var isEnter = ( 13 === e.which ), @@ -4923,7 +5617,7 @@ nonce: this.nonce.preview, customize_changeset_uuid: api.settings.changeset.uuid }; - if ( ! api.state( 'saved' ).get() ) { + if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) { queryVars.customize_autosaved = 'on'; } @@ -4959,13 +5653,15 @@ save: function( args ) { var previewer = this, deferred = $.Deferred(), - changesetStatus = 'publish', + changesetStatus = api.state( 'selectedChangesetStatus' ).get(), + selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(), processing = api.state( 'processing' ), submitWhenDoneProcessing, submit, modifiedWhileSaving = {}, invalidSettings = [], - invalidControls; + invalidControls = [], + invalidSettingLessControls = []; if ( args && args.status ) { changesetStatus = args.status; @@ -5004,17 +5700,34 @@ } } ); } ); - invalidControls = api.findControlsForSettings( invalidSettings ); + + /** + * Find all invalid setting less controls with notification type error. + */ + api.control.each( function( control ) { + if ( ! control.setting || ! control.setting.id && control.active.get() ) { + control.notifications.each( function( notification ) { + if ( 'error' === notification.type ) { + invalidSettingLessControls.push( [ control ] ); + } + } ); + } + } ); + + invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) ); if ( ! _.isEmpty( invalidControls ) ) { - _.values( invalidControls )[0][0].focus(); + + invalidControls[0][0].focus(); api.unbind( 'change', captureSettingModifiedDuringSave ); - api.notifications.add( errorCode, new api.Notification( errorCode, { - message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ), - type: 'error', - dismissible: true, - saveFailure: true - } ) ); + if ( invalidSettings.length ) { + api.notifications.add( errorCode, new api.Notification( errorCode, { + message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ), + type: 'error', + dismissible: true, + saveFailure: true + } ) ); + } deferred.rejectWith( previewer, [ { setting_invalidities: settingInvalidities } @@ -5031,9 +5744,13 @@ nonce: previewer.nonce.save, customize_changeset_status: changesetStatus } ); + if ( args && args.date ) { query.customize_changeset_date = args.date; + } else if ( 'future' === changesetStatus && selectedChangesetDate ) { + query.customize_changeset_date = selectedChangesetDate; } + if ( args && args.title ) { query.customize_changeset_title = args.title; } @@ -5070,6 +5787,13 @@ }); request.fail( function ( response ) { + var notification, notificationArgs; + notificationArgs = { + type: 'error', + dismissible: true, + fromServer: true, + saveFailure: true + }; if ( '0' === response ) { response = 'not_logged_in'; @@ -5087,23 +5811,23 @@ previewer.preview.iframe.show(); } ); } else if ( response.code ) { - api.notifications.add( response.code, new api.Notification( response.code, { - message: response.message, - type: 'error', - dismissible: true, - fromServer: true, - saveFailure: true - } ) ); + if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) { + api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus(); + } else { + notification = new api.Notification( response.code, _.extend( notificationArgs, { + message: response.message + } ) ); + } } else { - api.notifications.add( 'unknown_error', new api.Notification( 'unknown_error', { - message: api.l10n.serverSaveError, - type: 'error', - dismissible: true, - fromServer: true, - saveFailure: true + notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, { + message: api.l10n.serverSaveError } ) ); } + if ( notification ) { + api.notifications.add( notification.code, notification ); + } + if ( response.setting_validities ) { api._handleSettingValidities( { settingValidities: response.setting_validities, @@ -5113,6 +5837,14 @@ deferred.rejectWith( previewer, [ response ] ); api.trigger( 'error', response ); + + // Start a new changeset if the underlying changeset was published. + if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) { + api.settings.changeset.uuid = response.next_changeset_uuid; + api.state( 'changesetStatus' ).set( '' ); + parent.send( 'changeset-uuid', api.settings.changeset.uuid ); + api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid ); + } } ); request.done( function( response ) { @@ -5120,6 +5852,8 @@ previewer.send( 'saved', response ); api.state( 'changesetStatus' ).set( response.changeset_status ); + api.state( 'changesetDate' ).set( response.changeset_date ); + if ( 'publish' === response.changeset_status ) { // Mark all published as clean if they haven't been modified during the request. @@ -5173,6 +5907,28 @@ } return deferred.promise(); + }, + + /** + * Builds the front preview url with the current state of customizer. + * + * @since 4.9 + * + * @return {string} Preview url. + */ + getFrontendPreviewUrl: function() { + var previewer = this, + a = document.createElement( 'a' ), + params = {}; + + if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) { + params.customize_changeset_uuid = api.settings.changeset.uuid; + } + + a.href = previewer.previewUrl(); + a.search = $.param( params ); + + return a.href; } }); @@ -5299,47 +6055,90 @@ }); // Save and activated states - (function() { - var state = new api.Values(), - saved = state.create( 'saved' ), - saving = state.create( 'saving' ), - activated = state.create( 'activated' ), - processing = state.create( 'processing' ), - paneVisible = state.create( 'paneVisible' ), - expandedPanel = state.create( 'expandedPanel' ), - expandedSection = state.create( 'expandedSection' ), - changesetStatus = state.create( 'changesetStatus' ), - previewerAlive = state.create( 'previewerAlive' ), - editShortcutVisibility = state.create( 'editShortcutVisibility' ), + (function( state ) { + var saved = state.instance( 'saved' ), + saving = state.instance( 'saving' ), + activated = state.instance( 'activated' ), + processing = state.instance( 'processing' ), + paneVisible = state.instance( 'paneVisible' ), + expandedPanel = state.instance( 'expandedPanel' ), + expandedSection = state.instance( 'expandedSection' ), + changesetStatus = state.instance( 'changesetStatus' ), + selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ), + changesetDate = state.instance( 'changesetDate' ), + selectedChangesetDate = state.instance( 'selectedChangesetDate' ), + previewerAlive = state.instance( 'previewerAlive' ), + editShortcutVisibility = state.instance( 'editShortcutVisibility' ), populateChangesetUuidParam; state.bind( 'change', function() { var canSave; + btnWrapper.removeClass( 'button-see-me' ); + if ( ! activated() ) { saveBtn.val( api.l10n.activate ); closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); + publishSettingsBtn.prop( 'disabled', false ); } else if ( '' === changesetStatus.get() && saved() ) { - saveBtn.val( api.l10n.saved ); + if ( api.settings.changeset.currentUserCanPublish ) { + saveBtn.val( api.l10n.published ); + } else { + saveBtn.val( api.l10n.saved ); + } + publishSettingsBtn.prop( 'disabled', true ); closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); } else { - saveBtn.val( api.l10n.save ); + if ( 'draft' === selectedChangesetStatus() ) { + if ( saved() && selectedChangesetStatus() === changesetStatus() ) { + saveBtn.val( api.l10n.draftSaved ); + } else { + saveBtn.val( api.l10n.saveDraft ); + } + } else if ( 'future' === selectedChangesetStatus() ) { + if ( saved() && selectedChangesetStatus() === changesetStatus() ) { + if ( changesetDate.get() !== selectedChangesetDate.get() ) { + saveBtn.val( api.l10n.schedule ); + btnWrapper.addClass( 'button-see-me' ); + } else { + saveBtn.val( api.l10n.scheduled ); + } + } else { + btnWrapper.addClass( 'button-see-me' ); + saveBtn.val( api.l10n.schedule ); + } + } else if ( ! api.settings.changeset.currentUserCanPublish ) { + selectedChangesetStatus( 'draft' ); + } else { + saveBtn.val( api.l10n.publish ); + } closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); + publishSettingsBtn.prop( 'disabled', false ); } /* * Save (publish) button should be enabled if saving is not currently happening, * and if the theme is not active or the changeset exists but is not published. */ - canSave = ! saving() && ( ! activated() || ! saved() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) ); + canSave = ! saving() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) ); saveBtn.prop( 'disabled', ! canSave ); }); + selectedChangesetStatus.validate = function( status ) { + if ( '' === status || 'auto-draft' === status ) { + return null; + } + return status; + }; + // Set default states. changesetStatus( api.settings.changeset.status ); + changesetDate( api.settings.changeset.publishDate ); + selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? 'publish' : api.settings.changeset.status ); + selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection. saved( true ); if ( '' === changesetStatus() ) { // Handle case for loading starter content. api.each( function( setting ) { @@ -5424,18 +6223,32 @@ history.replaceState( {}, document.title, urlParser.href ); }; + /** + * Deactivate themes section if changeset status is not auto-draft + */ + api.section( 'themes', function( section ) { + var canActivate; + + canActivate = function() { + return ! changesetStatus() || 'auto-draft' === changesetStatus(); + }; + + section.active.validate = canActivate; + section.active.set( canActivate() ); + changesetStatus.bind( function() { + section.active.set( canActivate() ); + } ); + } ); + // Show changeset UUID in URL when in branching mode and there is a saved changeset. if ( api.settings.changeset.branching ) { changesetStatus.bind( function( newStatus ) { populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus ); } ); } + }( api.state ) ); - // Expose states to the API. - api.state = state; - }()); - - // Set up autosave prompt. + // Set up initial notifications. (function() { /** @@ -5522,12 +6335,10 @@ // Remove the notification once the user starts making changes. onStateChange = function() { api.notifications.remove( code ); - api.state( 'saved' ).unbind( onStateChange ); - api.state( 'saving' ).unbind( onStateChange ); + api.unbind( 'change', onStateChange ); api.state( 'changesetStatus' ).unbind( onStateChange ); }; - api.state( 'saved' ).bind( onStateChange ); - api.state( 'saving' ).bind( onStateChange ); + api.bind( 'change', onStateChange ); api.state( 'changesetStatus' ).bind( onStateChange ); } @@ -5553,18 +6364,22 @@ api.previewer.save(); event.preventDefault(); }).keydown( function( event ) { - if ( 9 === event.which ) // tab + if ( 9 === event.which ) { // Tab. return; - if ( 13 === event.which ) // enter + } + if ( 13 === event.which ) { // Enter. api.previewer.save(); + } event.preventDefault(); }); closeBtn.keydown( function( event ) { - if ( 9 === event.which ) // tab + if ( 9 === event.which ) { // Tab. return; - if ( 13 === event.which ) // enter + } + if ( 13 === event.which ) { // Enter. this.click(); + } event.preventDefault(); }); @@ -5939,7 +6754,7 @@ * since customize-loader.js will also use one. So autosave restorations are disabled * when customize-loader.js is used. */ - if ( isInsideIframe && isCleanState() ) { + if ( isInsideIframe || isCleanState() ) { clearedToClose.resolve(); } else if ( confirm( api.l10n.saveAlert ) ) { @@ -6221,6 +7036,88 @@ }); })(); + /** + * Publish settings section and controls. + */ + api.control( 'changeset_status', 'changeset_scheduled_date', function( statusControl, dateControl ) { + $.when( statusControl.deferred.embedded, dateControl.deferred.embedded ).done( function() { + var radioNodes, statusElement, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, timeArrivedPollingInterval = 1000; + + radioNodes = statusControl.container.find( 'input[type=radio][name]' ); + statusElement = new api.Element( radioNodes ); + statusControl.elements.push( statusElement ); + + statusElement.sync( api.state( 'selectedChangesetStatus' ) ); + statusElement.set( api.state( 'selectedChangesetStatus' ).get() ); + + dateControl.notifications.alt = true; + dateControl.deferred.embedded.done( function() { + api.state( 'selectedChangesetDate' ).sync( dateControl.setting ); + api.state( 'selectedChangesetDate' ).set( dateControl.setting() ); + } ); + + publishWhenTime = function() { + var publishSettingsSection; + + api.state( 'selectedChangesetStatus' ).set( 'publish' ); + publishSettingsSection = api.section( 'publish_settings' ); + if ( publishSettingsSection ) { + publishSettingsSection.collapse(); + } + api.previewer.save(); + }; + + // Start countdown for when the dateTime arrives, or clear interval when it is . + updateTimeArrivedPoller = function() { + var shouldPoll = ( + 'future' === api.state( 'changesetStatus' ).get() && + 'future' === api.state( 'selectedChangesetStatus' ).get() && + api.state( 'changesetDate' ).get() && + api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() && + api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0 + ); + + if ( shouldPoll && ! pollInterval ) { + pollInterval = setInterval( function() { + var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ); + api.state( 'remainingTimeToPublish' ).set( remainingTime ); + if ( remainingTime <= 0 ) { + clearInterval( pollInterval ); + pollInterval = 0; + publishWhenTime(); + } + }, timeArrivedPollingInterval ); + } else if ( ! shouldPoll && pollInterval ) { + clearInterval( pollInterval ); + pollInterval = 0; + } + }; + + api.state( 'changesetDate' ).bind( updateTimeArrivedPoller ); + api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller ); + api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller ); + api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller ); + updateTimeArrivedPoller(); + + // Ensure dateControl only appears when selected status is future. + dateControl.active.validate = function() { + return 'future' === statusElement.get(); + }; + toggleDateControl = function( value ) { + dateControl.active.set( 'future' === value ); + }; + toggleDateControl( statusElement.get() ); + statusElement.bind( toggleDateControl ); + + // Show notification on date control when status is future but it isn't a future date. + api.state( 'saving' ).bind( function( isSaving ) { + if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) { + dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() ); + } + } ); + } ); + } ); + // Toggle visibility of Header Video notice when active state change. api.control( 'header_video', function( headerVideoControl ) { headerVideoControl.deferred.embedded.done( function() { diff --git a/src/wp-includes/class-wp-customize-control.php b/src/wp-includes/class-wp-customize-control.php index b9c83c7575..6a6069efe5 100644 --- a/src/wp-includes/class-wp-customize-control.php +++ b/src/wp-includes/class-wp-customize-control.php @@ -747,3 +747,8 @@ require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add * WP_Customize_New_Menu_Control class. */ require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' ); + +/** + * WP_Customize_Date_Time_Control class. + */ +require_once( ABSPATH . WPINC . '/customize/class-wp-customize-date-time-control.php' ); diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index a37d431dc1..85e225138e 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -596,38 +596,10 @@ final class WP_Customize_Manager { * @since 4.9.0 */ public function establish_loaded_changeset() { - - /** - * Filters whether or not changeset branching is allowed. - * - * By default in core, when changeset branching is not allowed, changesets will operate - * linearly in that only one saved changeset will exist at a time (with a 'draft' or - * 'future' status). This makes the Customizer operate in a way that is similar to going to - * "edit" to one existing post: all users will be making changes to the same post, and autosave - * revisions will be made for that post. - * - * By contrast, when changeset branching is allowed, then the model is like users going - * to "add new" for a page and each user makes changes independently of each other since - * they are all operating on their own separate pages, each getting their own separate - * initial auto-drafts and then once initially saved, autosave revisions on top of that - * user's specific post. - * - * Since linear changesets are deemed to be more suitable for the majority of WordPress users, - * they are the default. For WordPress sites that have heavy site management in the Customizer - * by multiple users then branching changesets should be enabled by means of this filter. - * - * @since 4.9.0 - * - * @param bool $allow_branching Whether branching is allowed. If `false`, the default, - * then only one saved changeset exists at a time. - * @param WP_Customize_Manager $wp_customize Manager instance. - */ - $this->branching = apply_filters( 'customize_changeset_branching', $this->branching, $this ); - if ( empty( $this->_changeset_uuid ) ) { $changeset_uuid = null; - if ( ! $this->branching ) { + if ( ! $this->branching() ) { $unpublished_changeset_posts = $this->get_changeset_posts( array( 'post_status' => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ), 'exclude_restore_dismissed' => false, @@ -751,6 +723,58 @@ final class WP_Customize_Manager { return $this->settings_previewed; } + /** + * Gets whether data from a changeset's autosaved revision should be loaded if it exists. + * + * @since 4.9.0 + * @see WP_Customize_Manager::changeset_data() + * + * @return bool Is using autosaved changeset revision. + */ + public function autosaved() { + return $this->autosaved; + } + + /** + * Whether the changeset branching is allowed. + * + * @since 4.9.0 + * @see WP_Customize_Manager::establish_loaded_changeset() + * + * @return bool Is changeset branching. + */ + public function branching() { + + /** + * Filters whether or not changeset branching is allowed. + * + * By default in core, when changeset branching is not allowed, changesets will operate + * linearly in that only one saved changeset will exist at a time (with a 'draft' or + * 'future' status). This makes the Customizer operate in a way that is similar to going to + * "edit" to one existing post: all users will be making changes to the same post, and autosave + * revisions will be made for that post. + * + * By contrast, when changeset branching is allowed, then the model is like users going + * to "add new" for a page and each user makes changes independently of each other since + * they are all operating on their own separate pages, each getting their own separate + * initial auto-drafts and then once initially saved, autosave revisions on top of that + * user's specific post. + * + * Since linear changesets are deemed to be more suitable for the majority of WordPress users, + * they are the default. For WordPress sites that have heavy site management in the Customizer + * by multiple users then branching changesets should be enabled by means of this filter. + * + * @since 4.9.0 + * + * @param bool $allow_branching Whether branching is allowed. If `false`, the default, + * then only one saved changeset exists at a time. + * @param WP_Customize_Manager $wp_customize Manager instance. + */ + $this->branching = apply_filters( 'customize_changeset_branching', $this->branching, $this ); + + return $this->branching; + } + /** * Get the changeset UUID. * @@ -763,7 +787,7 @@ final class WP_Customize_Manager { */ public function changeset_uuid() { if ( empty( $this->_changeset_uuid ) ) { - throw new Exception( 'Changeset UUID has not been set.' ); // @todo Replace this with a call to `WP_Customize_Manager::establish_loaded_changeset()` during 4.9-beta2. + $this->establish_loaded_changeset(); } return $this->_changeset_uuid; } @@ -980,6 +1004,30 @@ final class WP_Customize_Manager { return get_posts( $args ); } + /** + * Dismiss all of the current user's auto-drafts (other than the present one). + * + * @since 4.9.0 + * @return int The number of auto-drafts that were dismissed. + */ + protected function dismiss_user_auto_draft_changesets() { + $changeset_autodraft_posts = $this->get_changeset_posts( array( + 'post_status' => 'auto-draft', + 'exclude_restore_dismissed' => true, + 'posts_per_page' => -1, + ) ); + $dismissed = 0; + foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) { + if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) { + continue; + } + if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) { + $dismissed++; + } + } + return $dismissed; + } + /** * Get the changeset post id for the loaded changeset. * @@ -1050,7 +1098,7 @@ final class WP_Customize_Manager { if ( ! $changeset_post_id ) { $this->_changeset_data = array(); } else { - if ( $this->autosaved ) { + if ( $this->autosaved() ) { $autosave_post = wp_get_post_autosave( $changeset_post_id ); if ( $autosave_post ) { $data = $this->get_changeset_post_data( $autosave_post->ID ); @@ -1972,7 +2020,7 @@ final class WP_Customize_Manager { $settings = array( 'changeset' => array( 'uuid' => $this->changeset_uuid(), - 'autosaved' => $this->autosaved, + 'autosaved' => $this->autosaved(), ), 'timeouts' => array( 'selectiveRefresh' => 250, @@ -2345,28 +2393,24 @@ final class WP_Customize_Manager { } } else { $response = $r; + $changeset_post = get_post( $this->changeset_post_id() ); // Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one. if ( $is_new_changeset ) { - $changeset_autodraft_posts = $this->get_changeset_posts( array( - 'post_status' => 'auto-draft', - 'exclude_restore_dismissed' => true, - 'posts_per_page' => -1, - ) ); - foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) { - if ( $autosave_autodraft_post->ID !== $this->changeset_post_id() ) { - update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ); - } - } + $this->dismiss_user_auto_draft_changesets(); } // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported. - $response['changeset_status'] = get_post_status( $this->changeset_post_id() ); + $response['changeset_status'] = $changeset_post->post_status; if ( $is_publish && 'trash' === $response['changeset_status'] ) { $response['changeset_status'] = 'publish'; } - if ( 'publish' === $response['changeset_status'] ) { + if ( 'future' === $response['changeset_status'] ) { + $response['changeset_date'] = $changeset_post->post_date; + } + + if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) { $response['next_changeset_uuid'] = wp_generate_uuid4(); } } @@ -2434,7 +2478,13 @@ final class WP_Customize_Manager { if ( $changeset_post_id ) { $existing_status = get_post_status( $changeset_post_id ); if ( 'publish' === $existing_status || 'trash' === $existing_status ) { - return new WP_Error( 'changeset_already_published' ); + return new WP_Error( + 'changeset_already_published', + __( 'The previous set of changes already been published. Please try saving your current set of changes again.' ), + array( + 'next_changeset_uuid' => wp_generate_uuid4(), + ) + ); } $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id ); @@ -2453,7 +2503,7 @@ final class WP_Customize_Manager { if ( $args['date_gmt'] ) { $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) ); if ( ! $is_future_dated ) { - return new WP_Error( 'not_future_date' ); // Only future dates are allowed. + return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed. } if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) { @@ -2468,7 +2518,7 @@ final class WP_Customize_Manager { // Fail if the new status is future but the existing post's date is not in the future. $changeset_post = get_post( $changeset_post_id ); if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) { - return new WP_Error( 'not_future_date' ); + return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); } } @@ -3056,24 +3106,11 @@ final class WP_Customize_Manager { $changeset_post_id = $this->changeset_post_id(); if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) { - $changeset_autodraft_posts = $this->get_changeset_posts( array( - 'post_status' => 'auto-draft', - 'exclude_restore_dismissed' => true, - 'posts_per_page' => -1, - ) ); - $dismissed = 0; - foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) { - if ( $autosave_autodraft_post->ID === $changeset_post_id ) { - continue; - } - if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) { - $dismissed++; - } - } + $dismissed = $this->dismiss_user_auto_draft_changesets(); if ( $dismissed > 0 ) { wp_send_json_success( 'auto_draft_dismissed' ); } else { - wp_send_json_error( 'no_autosave_to_delete', 404 ); + wp_send_json_error( 'no_auto_draft_to_delete', 404 ); } } else { $revision = wp_get_post_autosave( $changeset_post_id ); @@ -3089,7 +3126,7 @@ final class WP_Customize_Manager { wp_send_json_success( 'autosave_revision_deleted' ); } } else { - wp_send_json_error( 'no_autosave_to_delete', 404 ); + wp_send_json_error( 'no_autosave_revision_to_delete', 404 ); } } wp_send_json_error( 'unknown_error', 500 ); @@ -3516,6 +3553,21 @@ final class WP_Customize_Manager { <# } ); #> + changeset_post_id(); - if ( ! $this->saved_starter_content_changeset && ! $this->autosaved ) { + if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) { if ( $changeset_post_id ) { $autosave_revision_post = wp_get_post_autosave( $changeset_post_id ); } else { @@ -3893,15 +3945,25 @@ final class WP_Customize_Manager { } // Prepare Customizer settings to pass to JavaScript. + $changeset_post = null; + if ( $changeset_post_id ) { + $changeset_post = get_post( $changeset_post_id ); + } + $settings = array( 'changeset' => array( 'uuid' => $this->changeset_uuid(), - 'branching' => $this->branching, - 'autosaved' => $this->autosaved, + 'branching' => $this->branching(), + 'autosaved' => $this->autosaved(), 'hasAutosaveRevision' => ! empty( $autosave_revision_post ), 'latestAutoDraftUuid' => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null, - 'status' => $changeset_post_id ? get_post_status( $changeset_post_id ) : '', + 'status' => $changeset_post ? $changeset_post->post_status : '', + 'currentUserCanPublish' => current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ), + 'publishDate' => $changeset_post ? $changeset_post->post_date : '', // @todo Only if future status? Rename to just date? ), + 'initialServerDate' => current_time( 'mysql', false ), + 'initialServerTimestamp' => floor( microtime( true ) * 1000 ), + 'initialClientTimestamp' => -1, // To be set with JS below. 'timeouts' => array( 'windowRefresh' => 250, 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000, @@ -3957,6 +4019,7 @@ final class WP_Customize_Manager { ?> + +