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(
'
' . __( '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(
+ '' . __( 'This changeset has already been published and cannot be further modified.' ) . '
' .
@@ -132,14 +148,11 @@ do_action( 'customize_controls_print_scripts' );
+
= 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 {
?>
+
+