<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[41626] trunk: Customize: Introduce drafting and scheduling for Customizer changesets.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/41626">41626</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/41626","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-09-27 22:24:37 +0000 (Wed, 27 Sep 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Customize: Introduce drafting and scheduling for Customizer changesets.

* Incorporates code from the Customize Snapshots and Customize Posts feature plugins.
* Adds a new Publish Settings section for managing the changeset status, scheduled date, and frontend preview link.
* Updates Publish button to reflect the status selected in the Publish Settings (including Save Draft and Schedule).
* Deactivates the Themes section when a non-publish status selected, and deactivates the Publish Settings section when previewing a theme switch.
* Introduces an `outer` section type (`wp.customize.OuterSection` in JS) for the Publish Settings section to use and for available widgets and available nav menu panels to use in the future. These sections can be expanded while other sections are expanded.
* Introduces `WP_Customize_Date_Time_Control` in PHP and `wp.customize.DateTimeControl` in JS for managing a date/time value.
* Keeps track of scheduled time and proactively publish from the client when the time arrives, as opposed to waiting for WP Cron.
* Auto-publishes a scheduled changeset when attempting to access one that missed its schedule.
* Starts a new changeset if attempting to save a changeset that was previously publish.
* Adds `force` arg to `requestChangesetUpdate()` to force an update request even when there are no pending changes.
* Adds utils methods for `getCurrentTimestamp` and `getRemainingTime`.
* Adds new state values for `selectedChangesetStatus`, `changesetDate`, `selectedChangesetDate`.
* Fixes logic for when to short-circuit check to close Customizer when there are unsaved changes.
* Adds getter methods for `autosaved` and `branching` parameters, with the latter applying the `customize_changeset_branching` filter.
* Call to `establish_loaded_changeset` on the fly when `changeset_uuid()` is called if no changeset UUID was specififed.
* De-duplicates logic for dismissing auto-draft changesets.
* Includes unit tests.

Builds on <a href="https://core.trac.wordpress.org/changeset/41597">[41597]</a>.
Props sayedwp, westonruter, melchoyce, JoshuaWold, folletto, stubgo, karmatosed, dlh, paaljoachim, afercia, johnregan3, utkarshpatel, valendesigns.
See <a href="https://core.trac.wordpress.org/ticket/30937">#30937</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/39896">#39896</a>, <a href="https://core.trac.wordpress.org/ticket/28721">#28721</a>, <a href="https://core.trac.wordpress.org/ticket/39275">#39275</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsscustomizecontrolscss">trunk/src/wp-admin/css/customize-controls.css</a></li>
<li><a href="#trunksrcwpadmincustomizephp">trunk/src/wp-admin/customize.php</a></li>
<li><a href="#trunksrcwpadminjscustomizecontrolsjs">trunk/src/wp-admin/js/customize-controls.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizecontrolphp">trunk/src/wp-includes/class-wp-customize-control.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludesjscustomizepreviewjs">trunk/src/wp-includes/js/customize-preview.js</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunktestsphpunittestsajaxCustomizeManagerphp">trunk/tests/phpunit/tests/ajax/CustomizeManager.php</a></li>
<li><a href="#trunktestsphpunittestscustomizemanagerphp">trunk/tests/phpunit/tests/customize/manager.php</a></li>
<li><a href="#trunktestsqunitfixturescustomizesettingsjs">trunk/tests/qunit/fixtures/customize-settings.js</a></li>
<li><a href="#trunktestsqunitindexhtml">trunk/tests/qunit/index.html</a></li>
<li><a href="#trunktestsqunitwpadminjscustomizecontrolsjs">trunk/tests/qunit/wp-admin/js/customize-controls.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludescustomizeclasswpcustomizedatetimecontrolphp">trunk/src/wp-includes/customize/class-wp-customize-date-time-control.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadmincsscustomizecontrolscss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/customize-controls.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/customize-controls.css     2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/src/wp-admin/css/customize-controls.css       2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -24,11 +24,94 @@
</span><span class="cx" style="display: block; padding: 0 10px">        color: #555d66;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-header-actions .button-primary {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-save-button-wrapper {
</ins><span class="cx" style="display: block; padding: 0 10px">         float: right;
</span><span class="cx" style="display: block; padding: 0 10px">        margin-top: 9px;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#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;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-header-actions .spinner {
</span><span class="cx" style="display: block; padding: 0 10px">        margin-top: 13px;
</span><span class="cx" style="display: block; padding: 0 10px">        margin-right: 4px;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -53,10 +136,181 @@
</span><span class="cx" style="display: block; padding: 0 10px">        margin-bottom: 15px;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#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;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-info.section-meta {
</span><span class="cx" style="display: block; padding: 0 10px">        margin-bottom: 15px;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.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;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-info.is-in-view,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-section-title.is-in-view {
</span><span class="cx" style="display: block; padding: 0 10px">        position: absolute;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -105,6 +359,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-pane-child .customize-section-title h3,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-pane-child h3.customize-section-title,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-outer-theme-controls .customize-pane-child .customize-section-title h3,
+#customize-outer-theme-controls .customize-pane-child h3.customize-section-title,
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-info .panel-title {
</span><span class="cx" style="display: block; padding: 0 10px">        font-size: 20px;
</span><span class="cx" style="display: block; padding: 0 10px">        font-weight: 200;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -150,6 +406,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-info .customize-panel-description,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-info .customize-section-description,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-outer-theme-controls .customize-info .customize-section-description,
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .no-widget-areas-rendered-notice {
</span><span class="cx" style="display: block; padding: 0 10px">        color: #555d66;
</span><span class="cx" style="display: block; padding: 0 10px">        display: none;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -171,7 +428,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">        margin-bottom: 0;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-controls .customize-info .customize-section-description {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-controls .customize-info .customize-section-description,
+#customize-outer-theme-controls .customize-section-description {
</ins><span class="cx" style="display: block; padding: 0 10px">         margin-bottom: 15px;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -189,11 +447,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">        padding-right: 30px;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-theme-controls .control-section {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-theme-controls .control-section,
+#customize-outer-theme-controls .control-section {
</ins><span class="cx" style="display: block; padding: 0 10px">         border: none;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-theme-controls .accordion-section-title {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-theme-controls .accordion-section-title,
+#customize-outer-theme-controls .accordion-section-title {
</ins><span class="cx" style="display: block; padding: 0 10px">         color: #555d66;
</span><span class="cx" style="display: block; padding: 0 10px">        background-color: #fff;
</span><span class="cx" style="display: block; padding: 0 10px">        border-bottom: 1px solid #ddd;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -209,12 +469,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">        border-left: 4px solid #fff;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-theme-controls .accordion-section-title:after {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-theme-controls .accordion-section-title:after,
+#customize-outer-theme-controls .accordion-section-title:after {
</ins><span class="cx" style="display: block; padding: 0 10px">         content: "\f345";
</span><span class="cx" style="display: block; padding: 0 10px">        color: #a0a5aa;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-theme-controls .accordion-section-content {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-theme-controls .accordion-section-content,
+#customize-outer-theme-controls .accordion-section-content {
</ins><span class="cx" style="display: block; padding: 0 10px">         color: #555d66;
</span><span class="cx" style="display: block; padding: 0 10px">        background: transparent;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -222,6 +484,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .control-section:hover > .accordion-section-title,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .control-section .accordion-section-title:hover,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .control-section.open .accordion-section-title,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#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,
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .control-section .accordion-section-title:focus {
</span><span class="cx" style="display: block; padding: 0 10px">        color: #0073aa;
</span><span class="cx" style="display: block; padding: 0 10px">        background: #f3f3f5;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -242,7 +507,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-theme-controls .control-section:hover > .accordion-section-title:after,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-theme-controls .control-section .accordion-section-title:hover:after,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-theme-controls .control-section.open .accordion-section-title:after,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-theme-controls .control-section .accordion-section-title:focus:after {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#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 {
</ins><span class="cx" style="display: block; padding: 0 10px">         color: #0073aa;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -250,7 +519,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">        border-bottom: 1px solid #eee;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-theme-controls .control-section.open .accordion-section-title {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-theme-controls .control-section.open .accordion-section-title,
+#customize-outer-theme-controls .control-section.open .accordion-section-title {
</ins><span class="cx" style="display: block; padding: 0 10px">         border-bottom-color: #eee !important;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -828,6 +1098,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">        margin: 0;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.wp-full-overlay.collapsed #customize-controls #customize-notifications-area {
+       display: none !important;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-controls #customize-notifications-area,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-section-title > .customize-control-notifications-container,
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .panel-meta > .customize-control-notifications-container {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1119,20 +1393,62 @@
</span><span class="cx" style="display: block; padding: 0 10px">        animation: dice-color-change 3s infinite;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-@-webkit-keyframes dice-color-change {
-       0% { color: #d4b146; }
-       50% { color: #ef54b0; }
-       75% { color: #7190d3; }
-       100% { color: #d4b146; }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button-see-me {
+       -webkit-animation: bounce .7s 1;
+       animation: bounce .7s 1;
+       -webkit-transform-origin: center bottom;
+       transform-origin: center bottom;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-@keyframes dice-color-change {
-       0% { color: #d4b146; }
-       50% { color: #ef54b0; }
-       75% { color: #7190d3; }
-       100% { color: #d4b146; }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+@-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);
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+@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);
+       }
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> .customize-control-header .choice {
</span><span class="cx" style="display: block; padding: 0 10px">        position: relative;
</span><span class="cx" style="display: block; padding: 0 10px">        display: block;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1310,7 +1626,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .control-section-themes .accordion-section-title span.customize-action,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#customize-controls .customize-section-title span.customize-action {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-controls .customize-section-title span.customize-action,
+#customize-outer-theme-controls .customize-section-title span.customize-action {
</ins><span class="cx" style="display: block; padding: 0 10px">         font-size: 13px;
</span><span class="cx" style="display: block; padding: 0 10px">        display: block;
</span><span class="cx" style="display: block; padding: 0 10px">        font-weight: 400;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1843,6 +2160,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                line-height: 32px;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        .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;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px">         .wp-core-ui.wp-customizer .button {
</span><span class="cx" style="display: block; padding: 0 10px">                margin-top: 12px;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1853,7 +2191,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                width: 100%;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        .wp-full-overlay.expanded {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .wp-full-overlay.expanded,
+       .outer-section-open .wp-full-overlay.expanded {
</ins><span class="cx" style="display: block; padding: 0 10px">                 margin-left: 0;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1931,12 +2270,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">                margin-top: 12px;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        #customize-header-actions .button-primary {
-               margin-top: 6px;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ #publish-settings {
+               height: 31px;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        #customize-control-changeset_status label {
+               padding-top: 15px;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         body.adding-widget div#available-widgets,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        body.adding-menu-items div#available-menu-items {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ body.adding-menu-items div#available-menu-items,
+       body.outer-section-open div#customize-outer-theme-controls-wrapper {
</ins><span class="cx" style="display: block; padding: 0 10px">                 top: 46px;
</span><span class="cx" style="display: block; padding: 0 10px">                left: 0;
</span><span class="cx" style="display: block; padding: 0 10px">                z-index: 10;
</span></span></pre></div>
<a id="trunksrcwpadmincustomizephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/customize.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/customize.php  2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/src/wp-admin/customize.php    2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -27,15 +27,31 @@
</span><span class="cx" style="display: block; padding: 0 10px"> global $wp_scripts, $wp_customize;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $wp_customize->changeset_post_id() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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 ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_die(
</span><span class="cx" style="display: block; padding: 0 10px">                        '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
</span><span class="cx" style="display: block; padding: 0 10px">                        '<p>' . __( 'Sorry, you are not allowed to edit this changeset.' ) . '</p>',
</span><span class="cx" style="display: block; padding: 0 10px">                        403
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( in_array( get_post_status( $wp_customize->changeset_post_id() ), array( 'publish', 'trash' ), true ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       $missed_schedule = (
+               'future' === $changeset_post->post_status &&
+               get_post_time( 'G', true, $changeset_post ) < time()
+       );
+       if ( $missed_schedule ) {
+               wp_publish_post( $changeset_post->ID );
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_die(
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        '<h1>' . __( 'Your scheduled changes just published' ) . '</h1>' .
+                       '<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>',
+                       200
+               );
+       }
+
+       if ( in_array( get_post_status( $changeset_post->ID ), array( 'publish', 'trash' ), true ) ) {
+               wp_die(
</ins><span class="cx" style="display: block; padding: 0 10px">                         '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
</span><span class="cx" style="display: block; padding: 0 10px">                        '<p>' . __( 'This changeset has already been published and cannot be further modified.' ) . '</p>' .
</span><span class="cx" style="display: block; padding: 0 10px">                        '<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -132,14 +148,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> <div class="wp-full-overlay expanded">
</span><span class="cx" style="display: block; padding: 0 10px">        <form id="customize-controls" class="wrap wp-full-overlay-sidebar">
</span><span class="cx" style="display: block; padding: 0 10px">                <div id="customize-header-actions" class="wp-full-overlay-header">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        <?php
-                       $save_text = $wp_customize->is_theme_active() ? __( 'Save &amp; Publish' ) : __( 'Save &amp; 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 );
-                       ?>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 <?php $save_text = $wp_customize->is_theme_active() ? __( 'Publish' ) : __( 'Activate &amp; Publish' ); ?>
+                       <div id="customize-save-button-wrapper" class="customize-save-button-wrapper" >
+                               <?php submit_button( $save_text, 'primary save', 'save', false ); ?>
+                               <button id="publish-settings" class="publish-settings button-primary button dashicons dashicons-admin-generic" aria-label="<?php esc_attr_e( 'Publish Settings' ); ?>" aria-expanded="false" disabled></button>
+                       </div>
</ins><span class="cx" style="display: block; padding: 0 10px">                         <span class="spinner"></span>
</span><span class="cx" style="display: block; padding: 0 10px">                        <button type="button" class="customize-controls-preview-toggle">
</span><span class="cx" style="display: block; padding: 0 10px">                                <span class="controls"><?php _e( 'Customize' ); ?></span>
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -203,6 +216,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                </div>
</span><span class="cx" style="display: block; padding: 0 10px">        </form>
</span><span class="cx" style="display: block; padding: 0 10px">        <div id="customize-preview" class="wp-full-overlay-main"></div>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        <div id="customize-sidebar-outer-content">
+               <div id="customize-outer-theme-controls-wrapper">
+                       <div id="customize-outer-theme-controls">
+                               <ul class="customize-outer-pane-parent"><?php // Outer panel and sections are not implemented, but its here as a placeholder to avoid any side-effect in api.Section. ?></ul>
+                       </div>
+               </div>
+       </div>
</ins><span class="cx" style="display: block; padding: 0 10px">         <?php
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="trunksrcwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-controls.js       2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/src/wp-admin/js/customize-controls.js 2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -359,6 +359,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *                             If not provided, then the changes will still be obtained from unsaved dirty settings.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param {object}  [args] - Additional options for the save request.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @param {boolean} [args.force=false] - Send request to update even when there are no changes to submit. This can be used to request the latest status of the changeset on the server.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @param {string}  [args.title] - Title to update in the changeset. Optional.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param {string}  [args.date] - Date to update in the changeset. Optional.
</span><span class="cx" style="display: block; padding: 0 10px">         * @returns {jQuery.Promise} Promise resolving with the response data.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -370,7 +371,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                submittedArgs = _.extend( {
</span><span class="cx" style="display: block; padding: 0 10px">                        title: null,
</span><span class="cx" style="display: block; padding: 0 10px">                        date: null,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        autosave: false
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 autosave: false,
+                       force: false
</ins><span class="cx" style="display: block; padding: 0 10px">                 }, args );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( changes ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -388,15 +390,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Allow plugins to attach additional params to the settings.
+               api.trigger( 'changeset-save', submittedChanges, submittedArgs );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Short-circuit when there are no pending changes.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         deferred.resolve( {} );
</span><span class="cx" style="display: block; padding: 0 10px">                        return deferred.promise();
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Allow plugins to attach additional params to the settings.
-               api.trigger( 'changeset-save', submittedChanges, submittedArgs );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used. Status is also disallowed for revisions regardless.
</span><span class="cx" style="display: block; padding: 0 10px">                if ( submittedArgs.status ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -440,6 +442,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        api.state( 'changesetStatus' ).set( data.changeset_status );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       if ( data.changeset_date ) {
+                               api.state( 'changesetDate' ).set( data.changeset_date );
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         deferred.resolve( data );
</span><span class="cx" style="display: block; padding: 0 10px">                        api.trigger( 'changeset-saved', data );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -585,6 +592,48 @@
</span><span class="cx" style="display: block; padding: 0 10px">        };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Get current timestamp adjusted for server clock time.
+        *
+        * Same functionality as the `current_time( 'mysql', false )` function in PHP.
+        *
+        * @since 4.9.0
+        *
+        * @returns {int} Current timestamp.
+        */
+       api.utils.getCurrentTimestamp = function getCurrentTimestamp() {
+               var currentDate, currentClientTimestamp, timestampDifferential;
+               currentClientTimestamp = _.now();
+               currentDate = new Date( api.settings.initialServerDate.replace( /-/g, '/' ) );
+               timestampDifferential = currentClientTimestamp - api.settings.initialClientTimestamp;
+               timestampDifferential += api.settings.initialClientTimestamp - api.settings.initialServerTimestamp;
+               currentDate.setTime( currentDate.getTime() + timestampDifferential );
+               return currentDate.getTime();
+       };
+
+       /**
+        * Get remaining time of when the date is set.
+        *
+        * @since 4.9.0
+        *
+        * @param {string|int|Date} datetime - Date time or timestamp of the future date.
+        * @return {int} remainingTime - Remaining time in milliseconds.
+        */
+       api.utils.getRemainingTime = function getRemainingTime( datetime ) {
+               var millisecondsDivider = 1000, remainingTime, timestamp;
+               if ( datetime instanceof Date ) {
+                       timestamp = datetime.getTime();
+               } else if ( 'string' === typeof datetime ) {
+                       timestamp = ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime();
+               } else {
+                       timestamp = datetime;
+               }
+
+               remainingTime = timestamp - api.utils.getCurrentTimestamp();
+               remainingTime = Math.ceil( remainingTime / millisecondsDivider );
+               return remainingTime;
+       };
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Return browser supported `transitionend` event name.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1080,6 +1129,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        api.Section = Container.extend({
</span><span class="cx" style="display: block; padding: 0 10px">                containerType: 'section',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                containerParent: '#customize-theme-controls',
+               containerPaneParent: '.customize-pane-parent',
</ins><span class="cx" style="display: block; padding: 0 10px">                 defaults: {
</span><span class="cx" style="display: block; padding: 0 10px">                        title: '',
</span><span class="cx" style="display: block; padding: 0 10px">                        description: '',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1132,9 +1183,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                embed: function () {
</span><span class="cx" style="display: block; padding: 0 10px">                        var inject,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                section = this,
-                               container = $( '#customize-theme-controls' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         section = this;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        section.containerParent = api.ensure( section.containerParent );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // Watch for changes to the panel state
</span><span class="cx" style="display: block; padding: 0 10px">                        inject = function ( panelId ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                var parentContainer;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1148,19 +1200,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                                parentContainer.append( section.headContainer );
</span><span class="cx" style="display: block; padding: 0 10px">                                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                                        if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                                container.append( section.contentContainer );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                         section.containerParent.append( section.contentContainer );
</ins><span class="cx" style="display: block; padding: 0 10px">                                                         }
</span><span class="cx" style="display: block; padding: 0 10px">                                                        section.deferred.embedded.resolve();
</span><span class="cx" style="display: block; padding: 0 10px">                                                });
</span><span class="cx" style="display: block; padding: 0 10px">                                        } );
</span><span class="cx" style="display: block; padding: 0 10px">                                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                        // There is no panel, so embed the section in the root of the customizer
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 parentContainer = api.ensure( section.containerPaneParent );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         if ( ! section.headContainer.parent().is( parentContainer ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                parentContainer.append( section.headContainer );
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                container.append( section.contentContainer );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         section.containerParent.append( section.contentContainer );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         }
</span><span class="cx" style="display: block; padding: 0 10px">                                        section.deferred.embedded.resolve();
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1297,9 +1349,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                completeCallback: expand
</span><span class="cx" style="display: block; padding: 0 10px">                                        });
</span><span class="cx" style="display: block; padding: 0 10px">                                } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        api.panel.each( function( panel ) {
-                                               panel.collapse();
-                                       });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 if ( ! args.allowMultiple ) {
+                                               api.panel.each( function( panel ) {
+                                                       panel.collapse();
+                                               });
+                                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                                         expand();
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1840,6 +1894,125 @@
</span><span class="cx" style="display: block; padding: 0 10px">        });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Class wp.customize.OuterSection.
+        *
+        * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
+        * it would require custom handling.
+        *
+        * @since 4.9
+        *
+        * @constructor
+        * @augments wp.customize.Section
+        * @augments wp.customize.Container
+        */
+       api.OuterSection = api.Section.extend({
+
+               /**
+                * Initialize.
+                *
+                * @since 4.9.0
+                *
+                * @returns {void}
+                */
+               initialize: function() {
+                       var section = this;
+                       section.containerParent = '#customize-outer-theme-controls';
+                       section.containerPaneParent = '.customize-outer-pane-parent';
+                       return api.Section.prototype.initialize.apply( section, arguments );
+               },
+
+               /**
+                * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
+                * on other sections and panels.
+                *
+                * @since 4.9.0
+                *
+                * @param {Boolean}  expanded - The expanded state to transition to.
+                * @param {Object}   [args] - Args.
+                * @param {boolean}  [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
+                * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
+                * @param {Object}   [args.duration] - The duration for the animation.
+                */
+               onChangeExpanded: function( expanded, args ) {
+                       var section = this,
+                               container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
+                               content = section.contentContainer,
+                               backBtn = content.find( '.customize-section-back' ),
+                               sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
+                               body = $( 'body' ),
+                               expand, panel;
+
+                       body.toggleClass( 'outer-section-open', expanded );
+                       section.container.toggleClass( 'open', expanded );
+                       section.container.removeClass( 'busy' );
+                       api.section.each( function( _section ) {
+                               if ( 'outer' === _section.params.type && _section.id !== section.id ) {
+                                       _section.container.removeClass( 'open' );
+                               }
+                       } );
+
+                       if ( expanded && ! content.hasClass( 'open' ) ) {
+
+                               if ( args.unchanged ) {
+                                       expand = args.completeCallback;
+                               } else {
+                                       expand = $.proxy( function() {
+                                               section._animateChangeExpanded( function() {
+                                                       sectionTitle.attr( 'tabindex', '-1' );
+                                                       backBtn.attr( 'tabindex', '0' );
+
+                                                       backBtn.focus();
+                                                       content.css( 'top', '' );
+                                                       container.scrollTop( 0 );
+
+                                                       if ( args.completeCallback ) {
+                                                               args.completeCallback();
+                                                       }
+                                               } );
+
+                                               content.addClass( 'open' );
+                                       }, this );
+                               }
+
+                               if ( section.panel() ) {
+                                       api.panel( section.panel() ).expand({
+                                               duration: args.duration,
+                                               completeCallback: expand
+                                       });
+                               } else {
+                                       expand();
+                               }
+
+                       } else if ( ! expanded && content.hasClass( 'open' ) ) {
+                               if ( section.panel() ) {
+                                       panel = api.panel( section.panel() );
+                                       if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
+                                               panel.collapse();
+                                       }
+                               }
+                               section._animateChangeExpanded( function() {
+                                       backBtn.attr( 'tabindex', '-1' );
+                                       sectionTitle.attr( 'tabindex', '0' );
+
+                                       sectionTitle.focus();
+                                       content.css( 'top', '' );
+
+                                       if ( args.completeCallback ) {
+                                               args.completeCallback();
+                                       }
+                               } );
+
+                               content.removeClass( 'open' );
+
+                       } else {
+                               if ( args.completeCallback ) {
+                                       args.completeCallback();
+                               }
+                       }
+               }
+       });
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 4.1.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @class
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3960,6 +4133,450 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">        });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * Class wp.customize.DateTimeControl.
+        *
+        * @since 4.9.0
+        * @constructor
+        * @augments wp.customize.Control
+        * @augments wp.customize.Class
+        */
+       api.DateTimeControl = api.Control.extend({
+
+               dateInputs: {},
+               inputElements: {},
+               invalidDate: false,
+
+               /**
+                * Initialize behaviors.
+                *
+                * @since 4.9.0
+                * @returns {void}
+                */
+               ready: function ready() {
+                       var control = this;
+
+                       _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'updateMinutesForHour' );
+
+                       control.dateInputs = control.container.find( '.date-input' );
+
+                       // @todo This needs https://core.trac.wordpress.org/ticket/37964
+                       if ( ! control.setting ) {
+                               control.setting = new api.Value();
+                       }
+
+                       if ( ! control.setting.get() && control.params.defaultValue ) {
+                               control.setting.set( control.params.defaultValue );
+                       }
+
+                       control.dateInputs.each( function() {
+                               var input = $( this ), component, element;
+                               component = input.data( 'component' );
+                               element = new api.Element( input );
+                               element.validate = function( value ) {
+                                       return _.contains( [ 'am', 'pm' ], value ) ? value : parseInt( value, 10 );
+                               };
+                               control.inputElements[ component ] = element;
+                               control.elements.push( element );
+                       } );
+
+                       control.dateInputs.on( 'input', control.populateSetting );
+                       control.inputElements.month.bind( control.updateDaysForMonth );
+                       control.inputElements.year.bind( control.updateDaysForMonth );
+                       control.inputElements.hour.bind( control.updateMinutesForHour );
+                       control.populateDateInputs();
+               },
+
+               /**
+                * Parse datetime string.
+                *
+                * @since 4.9.0
+                * @param {string} datetime Date/Time string. Accepts Y-m-d H:i:s format.
+                * @param {boolean} twelveHourFormat If twelve hour format array is required.
+                * @returns {object|null} Returns object containing date components or null if parse error.
+                */
+               parseDateTime: function parseDateTime( datetime, twelveHourFormat ) {
+                       var matches, date, midDayHour = 12;
+
+                       if ( datetime ) {
+                               matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ );
+                       }
+
+                       if ( ! matches ) {
+                               return null;
+                       }
+
+                       matches.shift();
+
+                       date = {
+                               year: matches.shift(),
+                               month: matches.shift(),
+                               day: matches.shift(),
+                               hour: matches.shift(),
+                               minute: matches.shift(),
+                               second: matches.shift()
+                       };
+
+                       if ( twelveHourFormat ) {
+                               date.hour = parseInt( date.hour, 10 );
+                               date.ampm = date.hour >= 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 );
+                       }
+               }
+       });
+
</ins><span class="cx" style="display: block; padding: 0 10px">         // Change objects contained within the main customize object to Settings.
</span><span class="cx" style="display: block; padding: 0 10px">        api.defaultConstructor = api.Setting;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4059,7 +4676,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        customize_messenger_channel: previewFrame.query.customize_messenger_channel
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( ! api.state( 'saved' ).get() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 params.customize_autosaved = 'on';
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4660,11 +5277,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                background:          api.BackgroundControl,
</span><span class="cx" style="display: block; padding: 0 10px">                background_position: api.BackgroundPositionControl,
</span><span class="cx" style="display: block; padding: 0 10px">                theme:               api.ThemeControl,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                date_time:           api.DateTimeControl,
</ins><span class="cx" style="display: block; padding: 0 10px">                 code_editor:         api.CodeEditorControl
</span><span class="cx" style="display: block; padding: 0 10px">        };
</span><span class="cx" style="display: block; padding: 0 10px">        api.panelConstructor = {};
</span><span class="cx" style="display: block; padding: 0 10px">        api.sectionConstructor = {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                themes: api.ThemesSection
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         themes: api.ThemesSection,
+               outer: api.OuterSection
</ins><span class="cx" style="display: block; padding: 0 10px">         };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4836,6 +5455,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">                api.trigger( 'pane-contents-reflowed' );
</span><span class="cx" style="display: block; padding: 0 10px">        }, api );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // 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 );
+       });
+
</ins><span class="cx" style="display: block; padding: 0 10px">         $( function() {
</span><span class="cx" style="display: block; padding: 0 10px">                api.settings = window._wpCustomizeSettings;
</span><span class="cx" style="display: block; padding: 0 10px">                api.l10n = window._wpCustomizeControlsL10n;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4863,8 +5504,61 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        title = $( '#customize-info .panel-title.site-title' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        closeBtn = $( '.customize-controls-close' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        saveBtn = $( '#save' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        btnWrapper = $( '#customize-save-button-wrapper' ),
+                       publishSettingsBtn = $( '#publish-settings' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         footerActions = $( '#customize-footer-actions' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                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: '<li id="customize-control-' + previewLinkControlId + '" class="customize-control"></li>'
+                               }
+                       } );
+
+                       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();
+                           }
+                       } );
+               } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Prevent the form from saving when enter is pressed on an input or select element.
</span><span class="cx" style="display: block; padding: 0 10px">                $('#customize-controls').on( 'keydown', function( e ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        var isEnter = ( 13 === e.which ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4923,7 +5617,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        nonce: this.nonce.preview,
</span><span class="cx" style="display: block; padding: 0 10px">                                        customize_changeset_uuid: api.settings.changeset.uuid
</span><span class="cx" style="display: block; padding: 0 10px">                                };
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( ! api.state( 'saved' ).get() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         queryVars.customize_autosaved = 'on';
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4959,13 +5653,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        save: function( args ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                var previewer = this,
</span><span class="cx" style="display: block; padding: 0 10px">                                        deferred = $.Deferred(),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        changesetStatus = 'publish',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 changesetStatus = api.state( 'selectedChangesetStatus' ).get(),
+                                       selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(),
</ins><span class="cx" style="display: block; padding: 0 10px">                                         processing = api.state( 'processing' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        submitWhenDoneProcessing,
</span><span class="cx" style="display: block; padding: 0 10px">                                        submit,
</span><span class="cx" style="display: block; padding: 0 10px">                                        modifiedWhileSaving = {},
</span><span class="cx" style="display: block; padding: 0 10px">                                        invalidSettings = [],
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        invalidControls;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 invalidControls = [],
+                                       invalidSettingLessControls = [];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( args && args.status ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        changesetStatus = args.status;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5004,17 +5700,34 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                                } );
</span><span class="cx" style="display: block; padding: 0 10px">                                        } );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        invalidControls = api.findControlsForSettings( invalidSettings );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                                       /**
+                                        * 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 ) ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         if ( ! _.isEmpty( invalidControls ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                _.values( invalidControls )[0][0].focus();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                                               invalidControls[0][0].focus();
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 api.unbind( 'change', captureSettingModifiedDuringSave );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                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
-                                               } ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         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
+                                                       } ) );
+                                               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                deferred.rejectWith( previewer, [
</span><span class="cx" style="display: block; padding: 0 10px">                                                        { setting_invalidities: settingInvalidities }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5031,9 +5744,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                nonce: previewer.nonce.save,
</span><span class="cx" style="display: block; padding: 0 10px">                                                customize_changeset_status: changesetStatus
</span><span class="cx" style="display: block; padding: 0 10px">                                        } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                                         if ( args && args.date ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                query.customize_changeset_date = args.date;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        } else if ( 'future' === changesetStatus && selectedChangesetDate ) {
+                                               query.customize_changeset_date = selectedChangesetDate;
</ins><span class="cx" style="display: block; padding: 0 10px">                                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                                         if ( args && args.title ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                query.customize_changeset_title = args.title;
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5070,6 +5787,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                        request.fail( function ( response ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                var notification, notificationArgs;
+                                               notificationArgs = {
+                                                       type: 'error',
+                                                       dismissible: true,
+                                                       fromServer: true,
+                                                       saveFailure: true
+                                               };
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                if ( '0' === response ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        response = 'not_logged_in';
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5087,23 +5811,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                                previewer.preview.iframe.show();
</span><span class="cx" style="display: block; padding: 0 10px">                                                        } );
</span><span class="cx" style="display: block; padding: 0 10px">                                                } else if ( response.code ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                        api.notifications.add( response.code, new api.Notification( response.code, {
-                                                               message: response.message,
-                                                               type: 'error',
-                                                               dismissible: true,
-                                                               fromServer: true,
-                                                               saveFailure: true
-                                                       } ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 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
+                                                               } ) );
+                                                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                        api.notifications.add( 'unknown_error', new api.Notification( 'unknown_error', {
-                                                               message: api.l10n.serverSaveError,
-                                                               type: 'error',
-                                                               dismissible: true,
-                                                               fromServer: true,
-                                                               saveFailure: true
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
+                                                               message: api.l10n.serverSaveError
</ins><span class="cx" style="display: block; padding: 0 10px">                                                         } ) );
</span><span class="cx" style="display: block; padding: 0 10px">                                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                if ( notification ) {
+                                                       api.notifications.add( notification.code, notification );
+                                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 if ( response.setting_validities ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        api._handleSettingValidities( {
</span><span class="cx" style="display: block; padding: 0 10px">                                                                settingValidities: response.setting_validities,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5113,6 +5837,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                deferred.rejectWith( previewer, [ response ] );
</span><span class="cx" style="display: block; padding: 0 10px">                                                api.trigger( 'error', response );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                                               // 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 );
+                                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                                         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                        request.done( function( response ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5120,6 +5852,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                previewer.send( 'saved', response );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                api.state( 'changesetStatus' ).set( response.changeset_status );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                api.state( 'changesetDate' ).set( response.changeset_date );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 if ( 'publish' === response.changeset_status ) {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                        // Mark all published as clean if they haven't been modified during the request.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5173,6 +5907,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                return deferred.promise();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        },
+
+                       /**
+                        * 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;
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px">                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5299,47 +6055,90 @@
</span><span class="cx" style="display: block; padding: 0 10px">                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Save and activated states
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                (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' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         (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' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 populateChangesetUuidParam;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        state.bind( 'change', function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                var canSave;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                btnWrapper.removeClass( 'button-see-me' );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( ! activated() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        saveBtn.val( api.l10n.activate );
</span><span class="cx" style="display: block; padding: 0 10px">                                        closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        publishSettingsBtn.prop( 'disabled', false );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                } else if ( '' === changesetStatus.get() && saved() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        saveBtn.val( api.l10n.saved );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 if ( api.settings.changeset.currentUserCanPublish ) {
+                                               saveBtn.val( api.l10n.published );
+                                       } else {
+                                               saveBtn.val( api.l10n.saved );
+                                       }
+                                       publishSettingsBtn.prop( 'disabled', true );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        saveBtn.val( api.l10n.save );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 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 );
+                                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        publishSettingsBtn.prop( 'disabled', false );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                /*
</span><span class="cx" style="display: block; padding: 0 10px">                                 * Save (publish) button should be enabled if saving is not currently happening,
</span><span class="cx" style="display: block; padding: 0 10px">                                 * and if the theme is not active or the changeset exists but is not published.
</span><span class="cx" style="display: block; padding: 0 10px">                                 */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                canSave = ! saving() && ( ! activated() || ! saved() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         canSave = ! saving() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                saveBtn.prop( 'disabled', ! canSave );
</span><span class="cx" style="display: block; padding: 0 10px">                        });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        selectedChangesetStatus.validate = function( status ) {
+                               if ( '' === status || 'auto-draft' === status ) {
+                                       return null;
+                               }
+                               return status;
+                       };
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // Set default states.
</span><span class="cx" style="display: block; padding: 0 10px">                        changesetStatus( api.settings.changeset.status );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        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.
</ins><span class="cx" style="display: block; padding: 0 10px">                         saved( true );
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( '' === changesetStatus() ) { // Handle case for loading starter content.
</span><span class="cx" style="display: block; padding: 0 10px">                                api.each( function( setting ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5424,18 +6223,32 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                history.replaceState( {}, document.title, urlParser.href );
</span><span class="cx" style="display: block; padding: 0 10px">                        };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        /**
+                        * 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() );
+                               } );
+                       } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // Show changeset UUID in URL when in branching mode and there is a saved changeset.
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( api.settings.changeset.branching ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                changesetStatus.bind( function( newStatus ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus );
</span><span class="cx" style="display: block; padding: 0 10px">                                } );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                }( api.state ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        // Expose states to the API.
-                       api.state = state;
-               }());
-
-               // Set up autosave prompt.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Set up initial notifications.
</ins><span class="cx" style="display: block; padding: 0 10px">                 (function() {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5522,12 +6335,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                // Remove the notification once the user starts making changes.
</span><span class="cx" style="display: block; padding: 0 10px">                                onStateChange = function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                        api.notifications.remove( code );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        api.state( 'saved' ).unbind( onStateChange );
-                                       api.state( 'saving' ).unbind( onStateChange );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 api.unbind( 'change', onStateChange );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         api.state( 'changesetStatus' ).unbind( onStateChange );
</span><span class="cx" style="display: block; padding: 0 10px">                                };
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                api.state( 'saved' ).bind( onStateChange );
-                               api.state( 'saving' ).bind( onStateChange );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         api.bind( 'change', onStateChange );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 api.state( 'changesetStatus' ).bind( onStateChange );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5553,18 +6364,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        api.previewer.save();
</span><span class="cx" style="display: block; padding: 0 10px">                        event.preventDefault();
</span><span class="cx" style="display: block; padding: 0 10px">                }).keydown( function( event ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( 9 === event.which ) // tab
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( 9 === event.which ) { // Tab.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 return;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( 13 === event.which ) // enter
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 }
+                       if ( 13 === event.which ) { // Enter.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 api.previewer.save();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        }
</ins><span class="cx" style="display: block; padding: 0 10px">                         event.preventDefault();
</span><span class="cx" style="display: block; padding: 0 10px">                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                closeBtn.keydown( function( event ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( 9 === event.which ) // tab
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( 9 === event.which ) { // Tab.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 return;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( 13 === event.which ) // enter
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 }
+                       if ( 13 === event.which ) { // Enter.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 this.click();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        }
</ins><span class="cx" style="display: block; padding: 0 10px">                         event.preventDefault();
</span><span class="cx" style="display: block; padding: 0 10px">                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5939,7 +6754,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                 * since customize-loader.js will also use one. So autosave restorations are disabled
</span><span class="cx" style="display: block; padding: 0 10px">                                 * when customize-loader.js is used.
</span><span class="cx" style="display: block; padding: 0 10px">                                 */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( isInsideIframe && isCleanState() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( isInsideIframe || isCleanState() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         clearedToClose.resolve();
</span><span class="cx" style="display: block; padding: 0 10px">                                } else if ( confirm( api.l10n.saveAlert ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -6221,6 +7036,88 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        });
</span><span class="cx" style="display: block; padding: 0 10px">                })();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /**
+                * 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() );
+                                       }
+                               } );
+                       } );
+               } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Toggle visibility of Header Video notice when active state change.
</span><span class="cx" style="display: block; padding: 0 10px">                api.control( 'header_video', function( headerVideoControl ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        headerVideoControl.deferred.embedded.done( function() {
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizecontrolphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-control.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-control.php      2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/src/wp-includes/class-wp-customize-control.php        2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -747,3 +747,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * WP_Customize_New_Menu_Control class.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * WP_Customize_Date_Time_Control class.
+ */
+require_once( ABSPATH . WPINC . '/customize/class-wp-customize-date-time-control.php' );
</ins></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-manager.php      2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/src/wp-includes/class-wp-customize-manager.php        2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -596,38 +596,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function establish_loaded_changeset() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               /**
-                * 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 );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 if ( empty( $this->_changeset_uuid ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $changeset_uuid = null;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( ! $this->branching ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! $this->branching() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $unpublished_changeset_posts = $this->get_changeset_posts( array(
</span><span class="cx" style="display: block; padding: 0 10px">                                        'post_status' => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        'exclude_restore_dismissed' => false,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -752,6 +724,58 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * 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;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Get the changeset UUID.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -763,7 +787,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function changeset_uuid() {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( empty( $this->_changeset_uuid ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        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.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $this->establish_loaded_changeset();
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">                return $this->_changeset_uuid;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -981,6 +1005,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * 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;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Get the changeset post id for the loaded changeset.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1050,7 +1098,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! $changeset_post_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->_changeset_data = array();
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( $this->autosaved ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( $this->autosaved() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $autosave_post = wp_get_post_autosave( $changeset_post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( $autosave_post ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        $data = $this->get_changeset_post_data( $autosave_post->ID );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1972,7 +2020,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $settings = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'changeset' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'uuid' => $this->changeset_uuid(),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'autosaved' => $this->autosaved,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'autosaved' => $this->autosaved(),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'timeouts' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'selectiveRefresh' => 250,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2345,28 +2393,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                        $response = $r;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        $changeset_post = get_post( $this->changeset_post_id() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one.
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $is_new_changeset ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $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 );
-                                       }
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $this->dismiss_user_auto_draft_changesets();
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $response['changeset_status'] = $changeset_post->post_status;
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( $is_publish && 'trash' === $response['changeset_status'] ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $response['changeset_status'] = 'publish';
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( 'publish' === $response['changeset_status'] ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( 'future' === $response['changeset_status'] ) {
+                               $response['changeset_date'] = $changeset_post->post_date;
+                       }
+
+                       if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $response['next_changeset_uuid'] = wp_generate_uuid4();
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2434,7 +2478,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $changeset_post_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $existing_status = get_post_status( $changeset_post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                return new WP_Error( 'changeset_already_published' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         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(),
+                                       )
+                               );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2453,7 +2503,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $args['date_gmt'] ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( ! $is_future_dated ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                return new WP_Error( 'not_future_date' ); // Only future dates are allowed.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed.
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2468,7 +2518,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        // Fail if the new status is future but the existing post's date is not in the future.
</span><span class="cx" style="display: block; padding: 0 10px">                        $changeset_post = get_post( $changeset_post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                return new WP_Error( 'not_future_date' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3056,24 +3106,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $changeset_post_id = $this->changeset_post_id();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $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++;
-                               }
-                       }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $dismissed = $this->dismiss_user_auto_draft_changesets();
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( $dismissed > 0 ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                wp_send_json_success( 'auto_draft_dismissed' );
</span><span class="cx" style="display: block; padding: 0 10px">                        } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                wp_send_json_error( 'no_autosave_to_delete', 404 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         wp_send_json_error( 'no_auto_draft_to_delete', 404 );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                        $revision = wp_get_post_autosave( $changeset_post_id );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3089,7 +3126,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        wp_send_json_success( 'autosave_revision_deleted' );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                wp_send_json_error( 'no_autosave_to_delete', 404 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">                wp_send_json_error( 'unknown_error', 500 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3516,6 +3553,21 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                <# } ); #>
</span><span class="cx" style="display: block; padding: 0 10px">                        </ul>
</span><span class="cx" style="display: block; padding: 0 10px">                </script>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script type="text/html" id="tmpl-customize-preview-link-control" >
+                       <span class="customize-control-title">
+                               <label><?php esc_html_e( 'Share Preview Link' ); ?></label>
+                       </span>
+                       <span class="description customize-control-description"><?php esc_html_e( 'See how changes would look live on your website, and share the preview with people who can\'t access the Customizer.' ); ?></span>
+                       <div class="customize-control-notifications-container"></div>
+                       <div class="preview-link-wrapper">
+                               <label>
+                                       <span class="screen-reader-text"><?php esc_html_e( 'Preview Link' ); ?></span>
+                                       <a class="preview-control-element" data-component="link" href="" target=""></a>
+                                       <input readonly class="preview-control-element" data-component="input" value="test" >
+                                       <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="<?php esc_attr_e( 'Copy' ); ?>" data-copied-text="<?php esc_attr_e( 'Copied' ); ?>" ><?php esc_html_e( 'Copy' ); ?></button>
+                               </label>
+                       </div>
+               </script>
</ins><span class="cx" style="display: block; padding: 0 10px">                 <?php
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3877,7 +3929,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $autosave_revision_post = null;
</span><span class="cx" style="display: block; padding: 0 10px">                $autosave_autodraft_post = null;
</span><span class="cx" style="display: block; padding: 0 10px">                $changeset_post_id = $this->changeset_post_id();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! $this->saved_starter_content_changeset && ! $this->autosaved ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( $changeset_post_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $autosave_revision_post = wp_get_post_autosave( $changeset_post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                        } else {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3893,15 +3945,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Prepare Customizer settings to pass to JavaScript.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $changeset_post = null;
+               if ( $changeset_post_id ) {
+                       $changeset_post = get_post( $changeset_post_id );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $settings = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'changeset' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'uuid' => $this->changeset_uuid(),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'branching' => $this->branching,
-                               'autosaved' => $this->autosaved,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'branching' => $this->branching(),
+                               'autosaved' => $this->autosaved(),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'hasAutosaveRevision' => ! empty( $autosave_revision_post ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'latestAutoDraftUuid' => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'status' => $changeset_post_id ? get_post_status( $changeset_post_id ) : '',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         '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?
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'initialServerDate' => current_time( 'mysql', false ),
+                       'initialServerTimestamp' => floor( microtime( true ) * 1000 ),
+                       'initialClientTimestamp' => -1, // To be set with JS below.
</ins><span class="cx" style="display: block; padding: 0 10px">                         'timeouts' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'windowRefresh' => 250,
</span><span class="cx" style="display: block; padding: 0 10px">                                'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3957,6 +4019,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                ?>
</span><span class="cx" style="display: block; padding: 0 10px">                <script type="text/javascript">
</span><span class="cx" style="display: block; padding: 0 10px">                        var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        _wpCustomizeSettings.initialClientTimestamp = _.now();
</ins><span class="cx" style="display: block; padding: 0 10px">                         _wpCustomizeSettings.controls = {};
</span><span class="cx" style="display: block; padding: 0 10px">                        _wpCustomizeSettings.settings = {};
</span><span class="cx" style="display: block; padding: 0 10px">                        <?php
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4047,7 +4110,55 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->register_control_type( 'WP_Customize_Theme_Control' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->register_control_type( 'WP_Customize_Code_Editor_Control' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->register_control_type( 'WP_Customize_Date_Time_Control' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /* Publish Settings */
+
+               $this->add_section( 'publish_settings', array(
+                       'title' => __( 'Publish Settings' ),
+                       'priority' => 0,
+                       'capability' => 'customize',
+                       'type' => 'outer',
+                       'active_callback' => array( $this, 'is_theme_active' ),
+               ) );
+
+               /* Publish Settings Controls */
+               $status_choices = array(
+                       'publish' => __( 'Publish' ),
+                       'draft' => __( 'Save Draft' ),
+                       'future' => __( 'Schedule' ),
+               );
+
+               if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
+                       unset( $status_choices['publish'] );
+               }
+
+               $this->add_control( 'changeset_status', array(
+                       'section' => 'publish_settings',
+                       'settings' => array(),
+                       'type' => 'radio',
+                       'label' => __( 'Action' ),
+                       'choices' => $status_choices,
+                       'capability' => 'customize',
+               ) );
+
+               if ( $this->changeset_post_id() && 'future' === get_post_status( $this->changeset_post_id() ) ) {
+                       $initial_date = get_the_time( 'Y-m-d H:i:s', $this->changeset_post_id() );
+               } else {
+                       $initial_date = current_time( 'mysql', false );
+               }
+               $this->add_control( new WP_Customize_Date_Time_Control( $this, 'changeset_scheduled_date', array(
+                       'section' => 'publish_settings',
+                       'settings' => array(),
+                       'type' => 'date_time',
+                       'min_year' => date( 'Y' ),
+                       'allow_past_date' => false,
+                       'twelve_hour_format' => false !== stripos( get_option( 'time_format' ), 'a' ),
+                       'description' => __( 'Schedule your customization changes to publish ("go live") at a future date.' ),
+                       'capability' => 'customize',
+                       'default_value' => $initial_date,
+               ) ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /* Themes */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
</span></span></pre></div>
<a id="trunksrcwpincludescustomizeclasswpcustomizedatetimecontrolphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/customize/class-wp-customize-date-time-control.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/customize/class-wp-customize-date-time-control.php                          (rev 0)
+++ trunk/src/wp-includes/customize/class-wp-customize-date-time-control.php    2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,257 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Customize API: WP_Customize_Date_Time_Control class
+ *
+ * @package WordPress
+ * @subpackage Customize
+ * @since 4.9.0
+ */
+
+/**
+ * Customize Date Time Control class.
+ *
+ * @since 4.9.0
+ *
+ * @see WP_Customize_Control
+ */
+class WP_Customize_Date_Time_Control extends WP_Customize_Control {
+
+       /**
+        * Customize control type.
+        *
+        * @since 4.9.0
+        * @var string
+        */
+       public $type = 'date_time';
+
+       /**
+        * Minimum Year.
+        *
+        * @since 4.9.0
+        * @var integer
+        */
+       public $min_year = 1000;
+
+       /**
+        * Maximum Year.
+        *
+        * @since 4.9.0
+        * @var integer
+        */
+       public $max_year = 9999;
+
+       /**
+        * Allow past date, if set to false user can only select future date.
+        *
+        * @since 4.9.0
+        * @var boolean
+        */
+       public $allow_past_date = true;
+
+       /**
+        * If set to false the control will appear in 24 hour format,
+        * the value will still be saved in Y-m-d H:i:s format.
+        *
+        * @since 4.9.0
+        * @var boolean
+        */
+       public $twelve_hour_format = true;
+
+       /**
+        * Default date/time to be displayed in the control.
+        *
+        * @since 4.9.0
+        * @var string
+        */
+       public $default_value;
+
+       /**
+        * Don't render the control's content - it's rendered with a JS template.
+        *
+        * @since 4.9.0
+        */
+       public function render_content() {}
+
+       /**
+        * Export data to JS.
+        *
+        * @since 4.9.0
+        * @return array
+        */
+       public function json() {
+               $data = parent::json();
+
+               $data['maxYear'] = intval( $this->max_year );
+               $data['minYear'] = intval( $this->min_year );
+               $data['allowPastDate'] = $this->allow_past_date ? true : false;
+               $data['twelveHourFormat'] = $this->twelve_hour_format ? true : false;
+               $data['defaultValue'] = $this->default_value;
+
+               return $data;
+       }
+
+       /**
+        * Renders a JS template for the content of date time control.
+        *
+        * @since 4.9.0
+        */
+       public function content_template() {
+               $data = array_merge( $this->json(), $this->get_month_choices() );
+               $timezone_info = $this->get_timezone_info();
+               ?>
+
+               <# _.defaults( data, <?php echo wp_json_encode( $data ); ?> ); #>
+
+               <span class="customize-control-title">
+                       <label>{{ data.label }}</label>
+               </span>
+               <div class="customize-control-notifications-container"></div>
+               <span class="description customize-control-description">{{ data.description }}</span>
+               <div class="date-time-fields">
+                       <div class="day-row">
+                               <span class="title-day"><?php esc_html_e( 'Day' ); ?></span>
+                               <div class="day-fields clear">
+                                       <label class="month-field">
+                                               <span class="screen-reader-text"><?php esc_html_e( 'Month' ); ?></span>
+                                                       <select class="date-input month" data-component="month">
+                                                               <# _.each( data.month_choices, function( choice ) {
+                                                                       if ( _.isObject( choice ) && ! _.isUndefined( choice.text ) && ! _.isUndefined( choice.value ) ) {
+                                                                               text = choice.text;
+                                                                               value = choice.value;
+                                                                       }
+                                                                       #>
+                                                                       <option value="{{ value }}" >
+                                                                               {{ text }}
+                                                                       </option>
+                                                               <# } ); #>
+                                                       </select>
+                                       </label>
+                                       <label class="day-field">
+                                               <span class="screen-reader-text"><?php esc_html_e( 'Day' ); ?></span>
+                                               <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input day" data-component="day" min="1" max="31"" />
+                                       </label>
+                                       <span class="time-special-char date-time-separator">,</span>
+                                       <label class="year-field">
+                                               <span class="screen-reader-text"><?php esc_html_e( 'Year' ); ?></span>
+                                               <# var maxYearLength = String( data.maxYear ).length; #>
+                                               <input type="number" size="4" maxlength="{{ maxYearLength }}" autocomplete="off" class="date-input year" data-component="year" min="{{ data.minYear }}" max="{{ data.maxYear }}" />
+                                       </label>
+                               </div>
+                       </div>
+                       <div class="time-row clear">
+                               <span class="title-time"><?php esc_html_e( 'Time' ); ?></span>
+                               <div class="time-fields clear">
+                                       <label class="hour-field">
+                                               <span class="screen-reader-text"><?php esc_html_e( 'Hour' ); ?></span>
+                                               <# var maxHour = data.twelveHourFormat ? 12 : 24; #>
+                                               <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input hour" data-component="hour" min="1" max="{{ maxHour }}"" />
+                                       </label>
+                                       <span class="time-special-char date-time-separator">:</span>
+                                       <label class="minute-field">
+                                               <span class="screen-reader-text"><?php esc_html_e( 'Minute' ); ?></span>
+                                               <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input minute" data-component="minute" min="0" max="59" />
+                                       </label>
+                                       <# if ( data.twelveHourFormat ) { #>
+                                       <label class="am-pm-field">
+                                               <span class="screen-reader-text"><?php esc_html_e( 'AM / PM' ); ?></span>
+                                               <select class="date-input" data-component="ampm">
+                                                       <option value="am"><?php esc_html_e( 'AM' ); ?></option>
+                                                       <option value="pm"><?php esc_html_e( 'PM' ); ?></option>
+                                               </select>
+                                       </label>
+                                       <# } #>
+                                       <abbr class="date-timezone" aria-label="<?php esc_attr_e( 'Timezone' ); ?>" title="<?php echo esc_attr( $timezone_info['description'] ); ?>"><?php echo esc_html( $timezone_info['abbr'] ); ?></abbr>
+                               </div>
+                       </div>
+               </div>
+               <?php
+       }
+
+       /**
+        * Generate options for the month Select.
+        *
+        * Based on touch_time().
+        *
+        * @since 4.9.0
+        * @see touch_time()
+        *
+        * @return array
+        */
+       public function get_month_choices() {
+               global $wp_locale;
+               $months = array();
+               for ( $i = 1; $i < 13; $i++ ) {
+                       $month_text = $wp_locale->get_month_abbrev( $wp_locale->get_month( $i ) );
+
+                       /* translators: 1: month number (01, 02, etc.), 2: month abbreviation */
+                       $months[ $i ]['text'] = sprintf( __( '%1$s-%2$s' ), $i, $month_text );
+                       $months[ $i ]['value'] = $i;
+               }
+               return array(
+                       'month_choices' => $months,
+               );
+       }
+
+       /**
+        * Get timezone info.
+        *
+        * @since 4.9.0
+        *
+        * @return array abbr and description.
+        */
+       public function get_timezone_info() {
+               $tz_string = get_option( 'timezone_string' );
+               $timezone_info = array();
+
+               if ( $tz_string ) {
+                       try {
+                               $tz = new DateTimezone( $tz_string );
+                       } catch ( Exception $e ) {
+                               $tz = '';
+                       }
+
+                       if ( $tz ) {
+                               $now = new DateTime( 'now', $tz );
+                               $formatted_gmt_offset = sprintf( 'UTC%s', $this->format_gmt_offset( $tz->getOffset( $now ) / 3600 ) );
+                               $tz_name = str_replace( '_', ' ', $tz->getName() );
+                               $timezone_info['abbr'] = $now->format( 'T' );
+
+                               /* translators: 1: timezone name, 2: timezone abbreviation, 3: gmt offset  */
+                               $timezone_info['description'] = sprintf( __( 'Timezone is %1$s (%2$s), currently %3$s.' ), $tz_name, $timezone_info['abbr'], $formatted_gmt_offset );
+                       } else {
+                               $timezone_info['description'] = '';
+                       }
+               } else {
+                       $formatted_gmt_offset = $this->format_gmt_offset( intval( get_option( 'gmt_offset', 0 ) ) );
+                       $timezone_info['abbr'] = sprintf( 'UTC%s', $formatted_gmt_offset );
+
+                       /* translators: %s: UTC offset  */
+                       $timezone_info['description'] = sprintf( __( 'Timezone is %s.' ), $timezone_info['abbr'] );
+               }
+
+               return $timezone_info;
+       }
+
+       /**
+        * Format GMT Offset.
+        *
+        * @since 4.9.0
+        * @see wp_timezone_choice()
+        *
+        * @param float $offset Offset in hours.
+        * @return string Formatted offset.
+        */
+       public function format_gmt_offset( $offset ) {
+               if ( 0 <= $offset ) {
+                       $formatted_offset = '+' . (string) $offset;
+               } else {
+                       $formatted_offset = (string) $offset;
+               }
+               $formatted_offset = str_replace(
+                       array( '.25', '.5', '.75' ),
+                       array( ':15', ':30', ':45' ),
+                       $formatted_offset
+               );
+               return $formatted_offset;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesjscustomizepreviewjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/js/customize-preview.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-preview.js     2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/src/wp-includes/js/customize-preview.js       2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -672,7 +672,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $( function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                var bg, setValue;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         var bg, setValue, handleUpdatedChangesetUuid;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                api.settings = window._wpCustomizeSettings;
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! api.settings ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -765,28 +765,39 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        api.preview.send( 'scroll', $( window ).scrollTop() );
</span><span class="cx" style="display: block; padding: 0 10px">                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                api.preview.bind( 'saved', function( response ) {
-                       if ( response.next_changeset_uuid ) {
-                               api.settings.changeset.uuid = response.next_changeset_uuid;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /**
+                * Handle update to changeset UUID.
+                *
+                * @param {string} uuid - UUID.
+                * @returns {void}
+                */
+               handleUpdatedChangesetUuid = function( uuid ) {
+                       api.settings.changeset.uuid = uuid;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                // Update UUIDs in links and forms.
-                               $( document.body ).find( 'a[href], area' ).each( function() {
-                                       api.prepareLinkPreview( this );
-                               } );
-                               $( document.body ).find( 'form' ).each( function() {
-                                       api.prepareFormPreview( this );
-                               } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Update UUIDs in links and forms.
+                       $( document.body ).find( 'a[href], area' ).each( function() {
+                               api.prepareLinkPreview( this );
+                       } );
+                       $( document.body ).find( 'form' ).each( function() {
+                               api.prepareFormPreview( this );
+                       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                /*
-                                * Replace the UUID in the URL. Note that the wrapped history.replaceState()
-                                * will handle injecting the current api.settings.changeset.uuid into the URL,
-                                * so this is merely to trigger that logic.
-                                */
-                               if ( history.replaceState ) {
-                                       history.replaceState( currentHistoryState, '', location.href );
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 /*
+                        * Replace the UUID in the URL. Note that the wrapped history.replaceState()
+                        * will handle injecting the current api.settings.changeset.uuid into the URL,
+                        * so this is merely to trigger that logic.
+                        */
+                       if ( history.replaceState ) {
+                               history.replaceState( currentHistoryState, '', location.href );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                };
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                api.preview.bind( 'changeset-uuid', handleUpdatedChangesetUuid );
+
+               api.preview.bind( 'saved', function( response ) {
+                       if ( response.next_changeset_uuid ) {
+                               handleUpdatedChangesetUuid( response.next_changeset_uuid );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                         api.trigger( 'saved', response );
</span><span class="cx" style="display: block; padding: 0 10px">                } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/src/wp-includes/script-loader.php     2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -547,8 +547,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'customize-views',    "/wp-includes/js/customize-views.js",  array( 'jquery', 'underscore', 'imgareaselect', 'customize-models', 'media-editor', 'media-views' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">        did_action( 'init' ) && $scripts->localize( 'customize-controls', '_wpCustomizeControlsL10n', array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                'activate'           => __( 'Save &amp; Activate' ),
-               'save'               => __( 'Save &amp; Publish' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'activate'           => __( 'Activate &amp; Publish' ),
+               'save'               => __( 'Save &amp; Publish' ), // @todo Remove as not required.
+               'publish'            => __( 'Publish' ),
+               'published'          => __( 'Published' ),
+               'saveDraft'          => __( 'Save Draft' ),
+               'draftSaved'         => __( 'Draft Saved' ),
+               'updating'           => __( 'Updating' ),
+               'schedule'           => __( 'Schedule' ),
+               'scheduled'          => __( 'Scheduled' ),
+               'invalid'            => __( 'Invalid' ),
+               'saveBeforeShare'    => __( 'Please save your changes in order to share the preview.' ),
+               'futureDateError'    => __( 'You must supply a future date to schedule.' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 'saveAlert'          => __( 'The changes you made will be lost if you navigate away from this page.' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'saved'              => __( 'Saved' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'cancel'             => __( 'Cancel' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -563,7 +573,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'serverSaveError'    => __( 'Failed connecting to the server. Please try saving again.' ),
</span><span class="cx" style="display: block; padding: 0 10px">                /* translators: placeholder is URL to the Customizer to load the autosaved version */
</span><span class="cx" style="display: block; padding: 0 10px">                'autosaveNotice'     => __( 'There is a more recent autosave of your changes than the one you are previewing. <a href="%s">Restore the autosave</a>' ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                'videoHeaderNotice'   => __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'videoHeaderNotice'  => __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Used for overriding the file types allowed in plupload.
</span><span class="cx" style="display: block; padding: 0 10px">                'allowedFiles'       => __( 'Allowed Files' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'customCssError'     => wp_array_slice_assoc(
</span></span></pre></div>
<a id="trunktestsphpunittestsajaxCustomizeManagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/ajax/CustomizeManager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/ajax/CustomizeManager.php       2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/tests/phpunit/tests/ajax/CustomizeManager.php 2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -165,7 +165,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->make_ajax_call( 'customize_save' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertFalse( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'changeset_already_published', $this->_last_response_parsed['data']['code'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_update_post( array( 'ID' => $wp_customize->changeset_post_id(), 'post_status' => 'auto-draft' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_update_post( array(
+                       'ID' => $wp_customize->changeset_post_id(),
+                       'post_status' => 'auto-draft',
+               ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // User cannot edit.
</span><span class="cx" style="display: block; padding: 0 10px">                $post_type_obj = get_post_type_object( 'customize_changeset' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -222,8 +225,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->make_ajax_call( 'customize_save' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'future', get_post_status( $wp_customize->changeset_post_id() ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_update_post( array( 'ID' => $wp_customize->changeset_post_id(), 'post_status' => 'auto-draft' ) );
-
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_update_post( array(
+                       'ID' => $wp_customize->changeset_post_id(),
+                       'post_status' => 'auto-draft',
+               ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -254,7 +259,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        function test_save_success_publish_create() {
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = $this->set_up_valid_state();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Successful future.
</del><span class="cx" style="display: block; padding: 0 10px">                 $_POST['customize_changeset_status'] = 'publish';
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customize_changeset_title'] = 'Success Changeset';
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customize_changeset_data'] = wp_json_encode( array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -268,9 +272,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'publish', $this->_last_response_parsed['data']['changeset_status'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( 'Success Changeset', get_post( $wp_customize->changeset_post_id() )->post_title );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'Successful Site Title', get_option( 'blogname' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -295,7 +299,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = $this->set_up_valid_state( $uuid );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Successful future.
</del><span class="cx" style="display: block; padding: 0 10px">                 $_POST['customize_changeset_status'] = 'publish';
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customize_changeset_title'] = 'Published';
</span><span class="cx" style="display: block; padding: 0 10px">                $this->make_ajax_call( 'customize_save' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -304,6 +307,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'publish', $this->_last_response_parsed['data']['changeset_status'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( 'New Site Title', get_option( 'blogname' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'Published', get_post( $post_id )->post_title );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -336,6 +340,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customize_changeset_date'] = $future_date;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->make_ajax_call( 'customize_save' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( $this->_last_response_parsed['success'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'changeset_date', $this->_last_response_parsed['data'] );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $changeset_post_schedule = get_post( $post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $future_date, $changeset_post_schedule->post_date );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -344,6 +349,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customize_changeset_status'] = 'draft';
</span><span class="cx" style="display: block; padding: 0 10px">                $this->make_ajax_call( 'customize_save' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( $this->_last_response_parsed['success'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayNotHasKey( 'changeset_date', $this->_last_response_parsed['data'] );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $changeset_post_draft = get_post( $post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $future_date, $changeset_post_draft->post_date );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -351,6 +357,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customize_changeset_status'] = 'future';
</span><span class="cx" style="display: block; padding: 0 10px">                $this->make_ajax_call( 'customize_save' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( $this->_last_response_parsed['success'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'changeset_date', $this->_last_response_parsed['data'] );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $changeset_post_schedule = get_post( $post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $future_date, $changeset_post_schedule->post_date );
</span><span class="cx" style="display: block; padding: 0 10px">                // Success if draft with past date.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -380,7 +387,167 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customize_changeset_status'] = 'publish';
</span><span class="cx" style="display: block; padding: 0 10px">                $this->make_ajax_call( 'customize_save' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( $this->_last_response_parsed['success'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
+               $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $changeset_post_publish = get_post( $post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotEquals( $future_date, $changeset_post_publish->post_date );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               // Check response when trying to update an already-published post.
+               $this->assertEquals( 'trash', get_post_status( $post_id ) );
+               $_POST['customize_changeset_status'] = 'publish';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'changeset_already_published', $this->_last_response_parsed['data']['code'] );
+               $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
+               $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Test WP_Customize_Manager::save().
+        *
+        * @ticket 39896
+        * @covers WP_Customize_Manager::save()
+        */
+       public function test_save_autosave() {
+               $uuid = wp_generate_uuid4();
+
+               $post_id = $this->factory()->post->create( array(
+                       'post_name' => $uuid,
+                       'post_type' => 'customize_changeset',
+                       'post_status' => 'draft',
+                       'post_content' => wp_json_encode( array(
+                               'blogname' => array(
+                                       'value' => 'New Site Title',
+                               ),
+                       ) ),
+               ) );
+               $this->set_up_valid_state( $uuid );
+
+               $this->assertFalse( wp_get_post_autosave( $post_id ) );
+
+               $_POST['customize_changeset_data'] = wp_json_encode( array(
+                       'blogname' => array(
+                               'value' => 'Autosaved Site Title',
+                       ),
+               ) );
+
+               $_POST['customize_changeset_autosave'] = 'on';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'draft', $this->_last_response_parsed['data']['changeset_status'] );
+               $autosave_revision = wp_get_post_autosave( $post_id );
+               $this->assertInstanceOf( 'WP_Post', $autosave_revision );
+
+               $this->assertContains( 'New Site Title', get_post( $post_id )->post_content );
+               $this->assertContains( 'Autosaved Site Title', $autosave_revision->post_content );
+       }
+
+       /**
+        * Test request for dismissing autosave changesets.
+        *
+        * @ticket 39896
+        * @covers WP_Customize_Manager::handle_dismiss_changeset_autosave_request()
+        * @covers WP_Customize_Manager::dismiss_user_auto_draft_changesets()
+        */
+       public function test_handle_dismiss_changeset_autosave_request() {
+               $uuid = wp_generate_uuid4();
+               $wp_customize = $this->set_up_valid_state( $uuid );
+
+               $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'invalid_nonce', $this->_last_response_parsed['data'] );
+
+               $nonce = wp_create_nonce( 'dismiss_customize_changeset_autosave' );
+               $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
+               $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
+
+               $other_user_id = $this->factory()->user->create();
+
+               // Create auto-drafts.
+               $user_auto_draft_ids = array();
+               for ( $i = 0; $i < 3; $i++ ) {
+                       $user_auto_draft_ids[] = $this->factory()->post->create( array(
+                               'post_name' => wp_generate_uuid4(),
+                               'post_type' => 'customize_changeset',
+                               'post_status' => 'auto-draft',
+                               'post_author' => self::$admin_user_id,
+                               'post_content' => wp_json_encode( array() ),
+                       ) );
+               }
+               $other_user_auto_draft_ids = array();
+               for ( $i = 0; $i < 3; $i++ ) {
+                       $other_user_auto_draft_ids[] = $this->factory()->post->create( array(
+                               'post_name' => wp_generate_uuid4(),
+                               'post_type' => 'customize_changeset',
+                               'post_status' => 'auto-draft',
+                               'post_author' => $other_user_id,
+                               'post_content' => wp_json_encode( array() ),
+                       ) );
+               }
+               foreach ( array_merge( $user_auto_draft_ids, $other_user_auto_draft_ids ) as $post_id ) {
+                       $this->assertFalse( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
+               }
+               $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'auto_draft_dismissed', $this->_last_response_parsed['data'] );
+               foreach ( $user_auto_draft_ids as $post_id ) {
+                       $this->assertEquals( 'auto-draft', get_post_status( $post_id ) );
+                       $this->assertTrue( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
+               }
+               foreach ( $other_user_auto_draft_ids as $post_id ) {
+                       $this->assertEquals( 'auto-draft', get_post_status( $post_id ) );
+                       $this->assertFalse( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
+               }
+
+               // Subsequent test results in none dismissed.
+               $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
+
+               // Save a changeset as a draft.
+               $r = $wp_customize->save_changeset_post( array(
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Foo',
+                               ),
+                       ),
+                       'status' => 'draft',
+               ) );
+               $this->assertNotInstanceOf( 'WP_Error', $r );
+               $this->assertFalse( wp_get_post_autosave( $wp_customize->changeset_post_id() ) );
+               $this->assertContains( 'Foo', get_post( $wp_customize->changeset_post_id() )->post_content );
+
+               // Since no autosave yet, confirm no action.
+               $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
+
+               // Add the autosave revision.
+               $r = $wp_customize->save_changeset_post( array(
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Bar',
+                               ),
+                       ),
+                       'autosave' => true,
+               ) );
+               $this->assertNotInstanceOf( 'WP_Error', $r );
+               $autosave_revision = wp_get_post_autosave( $wp_customize->changeset_post_id() );
+               $this->assertInstanceOf( 'WP_Post', $autosave_revision );
+               $this->assertContains( 'Foo', get_post( $wp_customize->changeset_post_id() )->post_content );
+               $this->assertContains( 'Bar', $autosave_revision->post_content );
+
+               // Confirm autosave gets deleted.
+               $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'autosave_revision_deleted', $this->_last_response_parsed['data'] );
+               $this->assertFalse( wp_get_post_autosave( $wp_customize->changeset_post_id() ) );
+
+               // Since no autosave yet, confirm no action.
+               $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/customize/manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/manager.php   2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/tests/phpunit/tests/customize/manager.php     2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -120,7 +120,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $uuid, $wp_customize->changeset_uuid() );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $theme, $wp_customize->get_stylesheet() );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $messenger_channel, $wp_customize->get_messenger_channel() );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertFalse( $wp_customize->autosaved() );
+               $this->assertTrue( $wp_customize->branching() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => null,
+               ) );
+               $this->assertTrue( wp_is_uuid( $wp_customize->changeset_uuid(), 4 ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $theme = 'twentyfourteen';
</span><span class="cx" style="display: block; padding: 0 10px">                $messenger_channel = 'preview-456';
</span><span class="cx" style="display: block; padding: 0 10px">                $_REQUEST['theme'] = $theme;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -133,10 +140,38 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $_REQUEST['customize_theme'] = $theme;
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = new WP_Customize_Manager();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $theme, $wp_customize->get_stylesheet() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertNotEmpty( $wp_customize->changeset_uuid() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertTrue( wp_is_uuid( $wp_customize->changeset_uuid(), 4 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test constructor when deferring UUID.
+        *
+        * @ticket 39896
+        * @covers WP_Customize_Manager::establish_loaded_changeset()
+        * @covers WP_Customize_Manager::__construct()
+        */
+       public function test_constructor_deferred_changeset_uuid() {
+               $data = array(
+                       'blogname' => array(
+                               'value' => 'Test',
+                       ),
+               );
+               $uuid = wp_generate_uuid4();
+               $post_id = $this->factory()->post->create( array(
+                       'post_type' => 'customize_changeset',
+                       'post_name' => $uuid,
+                       'post_status' => 'draft',
+                       'post_content' => wp_json_encode( $data ),
+               ) );
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => false, // Cause UUID to be deferred.
+                       'branching' => false, // To cause drafted changeset to be autoloaded.
+               ) );
+               $this->assertEquals( $uuid, $wp_customize->changeset_uuid() );
+               $this->assertEquals( $post_id, $wp_customize->changeset_post_id() );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test WP_Customize_Manager::setup_theme() for admin screen.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers WP_Customize_Manager::setup_theme()
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -254,6 +289,45 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test WP_Customize_Manager::autosaved().
+        *
+        * @ticket 39896
+        * @covers WP_Customize_Manager::autosaved()
+        */
+       public function test_autosaved() {
+               $wp_customize = new WP_Customize_Manager();
+               $this->assertFalse( $wp_customize->autosaved() );
+
+               $wp_customize = new WP_Customize_Manager( array( 'autosaved' => false ) );
+               $this->assertFalse( $wp_customize->autosaved() );
+
+               $wp_customize = new WP_Customize_Manager( array( 'autosaved' => true ) );
+               $this->assertTrue( $wp_customize->autosaved() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::branching().
+        *
+        * @ticket 39896
+        * @covers WP_Customize_Manager::branching()
+        */
+       public function test_branching() {
+               $wp_customize = new WP_Customize_Manager();
+               $this->assertTrue( $wp_customize->branching(), 'Branching should default to true since it is original behavior in 4.7.' );
+
+               $wp_customize = new WP_Customize_Manager( array( 'branching' => false ) );
+               $this->assertFalse( $wp_customize->branching() );
+               add_filter( 'customize_changeset_branching', '__return_true' );
+               $this->assertTrue( $wp_customize->branching() );
+               remove_filter( 'customize_changeset_branching', '__return_true' );
+
+               $wp_customize = new WP_Customize_Manager( array( 'branching' => true ) );
+               $this->assertTrue( $wp_customize->branching() );
+               add_filter( 'customize_changeset_branching', '__return_false' );
+               $this->assertFalse( $wp_customize->branching() );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test WP_Customize_Manager::changeset_uuid().
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 30937
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -337,20 +411,56 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers WP_Customize_Manager::changeset_data()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_changeset_data() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $uuid = wp_generate_uuid4();
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( array(), $wp_customize->changeset_data() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $uuid = wp_generate_uuid4();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $data = array( 'blogname' => array( 'value' => 'Hello World' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $data = array(
+                       'blogname' => array( 'value' => 'Hello World' ),
+                       'blogdescription' => array( 'value' => 'Greet the world' ),
+               );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->factory()->post->create( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'post_name' => $uuid,
</span><span class="cx" style="display: block; padding: 0 10px">                        'post_type' => 'customize_changeset',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'post_status' => 'auto-draft',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'post_status' => 'draft',
</ins><span class="cx" style="display: block; padding: 0 10px">                         'post_content' => wp_json_encode( $data ),
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $data, $wp_customize->changeset_data() );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               // Autosave.
+               $wp_customize->set_post_value( 'blogname', 'Hola Mundo' );
+               $wp_customize->register_controls(); // That is, settings, so blogname setting is registered.
+               $r = $wp_customize->save_changeset_post( array(
+                       'autosave' => true,
+               ) );
+               $this->assertNotInstanceOf( 'WP_Error', $r );
+
+               // No change to data if not requesting autosave.
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'autosaved' => false,
+               ) );
+               $wp_customize->register_controls(); // That is, settings.
+               $this->assertFalse( $wp_customize->autosaved() );
+               $this->assertEquals( $data, $wp_customize->changeset_data() );
+
+               // No change to data if not requesting autosave.
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'autosaved' => true,
+               ) );
+               $this->assertTrue( $wp_customize->autosaved() );
+               $this->assertNotEquals( $data, $wp_customize->changeset_data() );
+               $this->assertEquals(
+                       array_merge(
+                               wp_list_pluck( $data, 'value' ),
+                               array( 'blogname' => 'Hola Mundo' )
+                       ),
+                       wp_list_pluck( $wp_customize->changeset_data(), 'value' )
+               );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1273,6 +1383,98 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test writing changesets when user supplies unchanged values.
+        *
+        * @ticket 39896
+        * @covers WP_Customize_Manager::save_changeset_post()
+        * @covers WP_Customize_Manager::grant_edit_post_capability_for_changeset()
+        */
+       public function test_save_changeset_post_with_autosave() {
+               wp_set_current_user( self::$admin_user_id );
+               $uuid = wp_generate_uuid4();
+               $changeset_post_id = wp_insert_post( array(
+                       'post_type' => 'customize_changeset',
+                       'post_content' => wp_json_encode( array(
+                               'blogname' => array(
+                                       'value' => 'Auto-draft Title',
+                               ),
+                       ) ),
+                       'post_author' => self::$admin_user_id,
+                       'post_name' => $uuid,
+                       'post_status' => 'auto-draft',
+               ) );
+
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+               ) );
+               $wp_customize->register_controls(); // And settings too.
+
+               // Autosave of an auto-draft overwrites original.
+               $wp_customize->save_changeset_post( array(
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Autosaved Auto-draft Title',
+                               ),
+                       ),
+                       'autosave' => true,
+               ) );
+               $this->assertFalse( wp_get_post_autosave( $changeset_post_id ) );
+               $this->assertContains( 'Autosaved Auto-draft Title', get_post( $changeset_post_id )->post_content );
+
+               // Update status to draft for subsequent tests.
+               $wp_customize->save_changeset_post( array(
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Draft Title',
+                               ),
+                       ),
+                       'status' => 'draft',
+                       'autosave' => false,
+               ) );
+               $this->assertContains( 'Draft Title', get_post( $changeset_post_id )->post_content );
+
+               // Fail: illegal_autosave_with_date_gmt.
+               $r = $wp_customize->save_changeset_post( array(
+                       'autosave' => true,
+                       'date_gmt' => ( gmdate( 'Y' ) + 1 ) . '-12-01 00:00:00',
+               ) );
+               $this->assertInstanceOf( 'WP_Error', $r );
+               $this->assertEquals( 'illegal_autosave_with_date_gmt', $r->get_error_code() );
+
+               // Fail: illegal_autosave_with_status.
+               $r = $wp_customize->save_changeset_post( array(
+                       'autosave' => true,
+                       'status' => 'pending',
+               ) );
+               $this->assertEquals( 'illegal_autosave_with_status', $r->get_error_code() );
+
+               // Fail: illegal_autosave_with_non_current_user.
+               $r = $wp_customize->save_changeset_post( array(
+                       'autosave' => true,
+                       'user_id' => $this->factory()->user->create( array( 'role' => 'administrator' ) ),
+               ) );
+               $this->assertEquals( 'illegal_autosave_with_non_current_user', $r->get_error_code() );
+
+               // Try autosave.
+               $this->assertFalse( wp_get_post_autosave( $changeset_post_id ) );
+               $r = $wp_customize->save_changeset_post( array(
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Autosave Title',
+                               ),
+                       ),
+                       'autosave' => true,
+               ) );
+               $this->assertInternalType( 'array', $r );
+
+               // Verify that autosave happened.
+               $autosave_revision = wp_get_post_autosave( $changeset_post_id );
+               $this->assertInstanceOf( 'WP_Post', $autosave_revision );
+               $this->assertContains( 'Draft Title', get_post( $changeset_post_id )->post_content );
+               $this->assertContains( 'Autosave Title', $autosave_revision->post_content );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test passing `null` for a setting ID to remove it from the changeset.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 41621
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2351,10 +2553,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $data = json_decode( $json, true );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotEmpty( $data );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts' ), array_keys( $data ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts', 'initialClientTimestamp', 'initialServerDate', 'initialServerTimestamp' ), array_keys( $data ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( $autofocus, $data['autofocus'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'save', $data['nonce'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'preview', $data['nonce'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               $this->assertEqualSets(
+                       array(
+                               'branching',
+                               'autosaved',
+                               'hasAutosaveRevision',
+                               'latestAutoDraftUuid',
+                               'status',
+                               'uuid',
+                               'currentUserCanPublish',
+                               'publishDate',
+                       ),
+                       array_keys( $data['changeset'] )
+               );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="trunktestsqunitfixturescustomizesettingsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/fixtures/customize-settings.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/fixtures/customize-settings.js  2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/tests/qunit/fixtures/customize-settings.js    2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -106,6 +106,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'title': 'Fixture section of custom type re-using default template',
</span><span class="cx" style="display: block; padding: 0 10px">                        'type': 'reusing-default-template'
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'publish_settings': {
+                       'active': true,
+                       'description': '',
+                       'instanceNumber': 6,
+                       'priority': 20,
+                       'title': 'Fixture section of custom type re-using default template',
+                       'type': 'outer'
+               },
</ins><span class="cx" style="display: block; padding: 0 10px">                 'fixture-section-without-params': {}
</span><span class="cx" style="display: block; padding: 0 10px">        },
</span><span class="cx" style="display: block; padding: 0 10px">        'settings': {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -148,9 +156,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'label': 'Enter mobile preview mode'
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">        },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        initialClientTimestamp: 1506510531595,
+       initialServerDate: '2017-09-27 16:38:49',
+       initialServerTimestamp: 1506510529913,
</ins><span class="cx" style="display: block; padding: 0 10px">         changeset: {
</span><span class="cx" style="display: block; padding: 0 10px">                status: '',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                uuid: '0c674ff4-c159-4e7a-beb4-cb830ae73979'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         uuid: '0c674ff4-c159-4e7a-beb4-cb830ae73979',
+               autosaved: false,
+               branching: false,
+               currentUserCanPublish: false,
+               hasAutosaveRevision: false,
+               latestAutoDraftUuid: '341b06f6-3c1f-454f-96df-3cf197f3e347',
+               publishDate: ''
</ins><span class="cx" style="display: block; padding: 0 10px">         },
</span><span class="cx" style="display: block; padding: 0 10px">        timeouts: {
</span><span class="cx" style="display: block; padding: 0 10px">                windowRefresh: 250,
</span></span></pre></div>
<a id="trunktestsqunitindexhtml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/index.html</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/index.html      2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/tests/qunit/index.html        2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -872,7 +872,88 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                </div>
</span><span class="cx" style="display: block; padding: 0 10px">                        <# } #>
</span><span class="cx" style="display: block; padding: 0 10px">                </script>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        <script type="text/html" id="tmpl-customize-control-date_time-content">
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <# _.defaults( data, {"settings":[],"type":"date_time","priority":10,"active":true,"section":"","content":"<li id=\"customize-control-temp\" class=\"customize-control customize-control-date_time\">\n\t\t\t\t\t<\/li>","label":"","description":"","instanceNumber":69,"maxYear":9999,"minYear":1000,"allowPastDate":true,"twelveHourFormat":true,"defaultValue":null,"month_choices":{"1":{"text":"1-Jan","value":1},"2":{"text":"2-Feb","value":2},"3":{"text":"3-Mar","value":3},"4":{"text":"4-Apr","value":4},&q
 uot;5":{"text":"5-May","value":5},"6":{"text":"6-Jun","value":6},"7":{"text":"7-Jul","value":7},"8":{"text":"8-Aug","value":8},"9":{"text":"9-Sep","value":9},"10":{"text":"10-Oct","value":10},"11":{"text":"11-Nov","value":11},"12":{"text":"12-Dec","value":12}}} ); #>
+
+               <span class="customize-control-title">
+                       <label>{{ data.label }}</label>
+               </span>
+               <div class="customize-control-notifications-container"></div>
+               <span class="description customize-control-description">{{ data.description }}</span>
+               <div class="date-time-fields">
+                       <div class="day-row">
+                               <span class="title-day">Day</span>
+                               <div class="day-fields clear">
+                                       <label class="month-field">
+                                               <span class="screen-reader-text">Month</span>
+                                               <select class="date-input month" data-component="month">
+                                                       <# _.each( data.month_choices, function( choice ) {
+                                                                       if ( _.isObject( choice ) && ! _.isUndefined( choice.text ) && ! _.isUndefined( choice.value ) ) {
+                                                                       text = choice.text;
+                                                                       value = choice.value;
+                                                                       }
+                                                                       #>
+                                                               <option value="{{ value }}" >
+                                                                       {{ text }}
+                                                               </option>
+                                                               <# } ); #>
+                                               </select>
+                                       </label>
+                                       <label class="day-field">
+                                               <span class="screen-reader-text">Day</span>
+                                               <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input day" data-component="day" min="1" max="31"" />
+                                       </label>
+                                       <span class="time-special-char date-time-separator">,</span>
+                                       <label class="year-field">
+                                               <span class="screen-reader-text">Year</span>
+                                               <# var maxYearLength = String( data.maxYear ).length; #>
+                                                       <input type="number" size="4" maxlength="{{ maxYearLength }}" autocomplete="off" class="date-input year" data-component="year" min="{{ data.minYear }}" max="{{ data.maxYear }}" />
+                                       </label>
+                               </div>
+                       </div>
+                       <div class="time-row clear">
+                               <span class="title-time">Time</span>
+                               <div class="time-fields clear">
+                                       <label class="hour-field">
+                                               <span class="screen-reader-text">Hour</span>
+                                               <# var maxHour = data.twelveHourFormat ? 12 : 24; #>
+                                                       <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input hour" data-component="hour" min="1" max="{{ maxHour }}"" />
+                                       </label>
+                                       <span class="time-special-char date-time-separator">:</span>
+                                       <label class="minute-field">
+                                               <span class="screen-reader-text">Minute</span>
+                                               <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input minute" data-component="minute" min="0" max="59" />
+                                       </label>
+                                       <# if ( data.twelveHourFormat ) { #>
+                                               <label class="am-pm-field">
+                                                       <span class="screen-reader-text">AM / PM</span>
+                                                       <select class="date-input" data-component="ampm">
+                                                               <option value="am">AM</option>
+                                                               <option value="pm">PM</option>
+                                                       </select>
+                                               </label>
+                                               <# } #>
+                                       <abbr class="date-timezone" aria-label="Timezone" title="Timezone is Asia/Kolkata (IST), currently UTC+5:30.">IST</abbr>
+                               </div>
+                       </div>
+               </div>
+       </script>
+       <script type="text/html" id="tmpl-customize-preview-link-control" >
+               <span class="customize-control-title">
+               <label>Share Preview Link</label>
+       </span>
+               <span class="description customize-control-description">See how changes would look live on your website, and share the preview with people who can&#039;t access the Customizer.</span>
+               <div class="customize-control-notifications-container"></div>
+               <div class="preview-link-wrapper">
+                       <label>
+                               <span class="screen-reader-text">Preview Link</span>
+                               <a class="preview-control-element" data-component="link" href="" target=""></a>
+                               <input readonly class="preview-control-element" data-component="input" value="test" >
+                               <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="Copy" data-copied-text="Copied" >Copy</button>
+                       </label>
+               </div>
+       </script>
</ins><span class="cx" style="display: block; padding: 0 10px">         <script type="text/html" id="tmpl-media-modal">
</span><span class="cx" style="display: block; padding: 0 10px">                <div class="media-modal wp-core-ui">
</span><span class="cx" style="display: block; padding: 0 10px">                        <button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text">Close media panel</span></span></button>
</span></span></pre></div>
<a id="trunktestsqunitwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/customize-controls.js       2017-09-27 21:43:59 UTC (rev 41625)
+++ trunk/tests/qunit/wp-admin/js/customize-controls.js 2017-09-27 22:24:37 UTC (rev 41626)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -675,4 +675,256 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        jQuery.ajaxSetup( { beforeSend: originalBeforeSetup } );
</span><span class="cx" style="display: block; padding: 0 10px">                } );
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       module( 'Customize Utils: wp.customize.utils.getRemainingTime()' );
+       test( 'utils.getRemainingTime calculates time correctly', function( assert ) {
+               var datetime = '2599-08-06 12:12:13', timeRemaining, timeRemainingWithDateInstance, timeRemaingingWithTimestamp;
+
+               timeRemaining = wp.customize.utils.getRemainingTime( datetime );
+               timeRemainingWithDateInstance = wp.customize.utils.getRemainingTime( new Date( datetime.replace( /-/g, '/' ) ) );
+               timeRemaingingWithTimestamp = wp.customize.utils.getRemainingTime( ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime() );
+
+               assert.equal( typeof timeRemaining, 'number', timeRemaining );
+               assert.equal( typeof timeRemainingWithDateInstance, 'number', timeRemaining );
+               assert.equal( typeof timeRemaingingWithTimestamp, 'number', timeRemaining );
+               assert.deepEqual( timeRemaining, timeRemainingWithDateInstance );
+               assert.deepEqual( timeRemaining, timeRemaingingWithTimestamp );
+       });
+
+       module( 'Customize Utils: wp.customize.utils.getCurrentTimestamp()' );
+       test( 'utils.getCurrentTimestamp returns timestamp', function( assert ) {
+               var currentTimeStamp;
+               currentTimeStamp = wp.customize.utils.getCurrentTimestamp();
+               assert.equal( typeof currentTimeStamp, 'number' );
+       });
+
+       module( 'Customize Controls: wp.customize.DateTimeControl' );
+       test( 'Test DateTimeControl creation and its methods', function( assert ) {
+               var control, controlId = 'date_time', section, sectionId = 'fixture-section',
+                       datetime = '2599-08-06 18:12:13', dateTimeArray, dateTimeArrayInampm, timeString,
+                       day, year, month, minute, ampm, hour;
+
+               section = wp.customize.section( sectionId );
+
+               control = new wp.customize.DateTimeControl( controlId, {
+                       params: {
+                               section: section.id,
+                               type: 'date_time',
+                               content: '<li id="customize-control-' + controlId + '" class="customize-control"></li>',
+                               defaultValue: datetime
+                       }
+               } );
+
+               wp.customize.control.add( controlId, control );
+
+               // Test control creations.
+               assert.ok( control.templateSelector, '#customize-control-date_time-content' );
+               assert.ok( control.section(), sectionId );
+               assert.equal( _.size( control.inputElements ), control.elements.length );
+               assert.ok( control.setting(), datetime );
+
+               day = control.inputElements.day;
+               month = control.inputElements.month;
+               year = control.inputElements.year;
+               minute = control.inputElements.minute;
+               hour = control.inputElements.hour;
+               ampm = control.inputElements.ampm;
+
+               year( '23' );
+               assert.equal( typeof year(), 'number', 'Should always return integer' );
+
+               month( 'test' );
+               assert.notOk( month(), 'Should not accept text' );
+
+               // Test control.parseDateTime();
+               dateTimeArray = control.parseDateTime( datetime );
+               assert.deepEqual( dateTimeArray, {
+                       year: '2599',
+                       month: '08',
+                       hour: '18',
+                       minute: '12',
+                       second: '13',
+                       day: '06'
+               } );
+
+               dateTimeArrayInampm = control.parseDateTime( datetime, true );
+               assert.deepEqual( dateTimeArrayInampm, {
+                       year: '2599',
+                       month: '08',
+                       hour: '6',
+                       minute: '12',
+                       ampm: 'pm',
+                       day: '06'
+               } );
+
+               year( '2010' );
+               month( '12' );
+               day( '18' );
+               hour( '3' );
+               minute( '44' );
+               ampm( 'am' );
+
+               // Test control.convertInputDateToString().
+               timeString = control.convertInputDateToString();
+               assert.equal( timeString, '2010-12-18 03:44:00' );
+
+               ampm( 'pm' );
+               timeString = control.convertInputDateToString();
+               assert.equal( timeString, '2010-12-18 15:44:00' );
+
+               // Test control.updateDaysForMonth();.
+               year( 2017 );
+               month( 2 );
+               day( 31 );
+               control.updateDaysForMonth();
+               assert.deepEqual( day(), 28, 'Should update to the correct days' );
+
+               day( 20 );
+               assert.deepEqual( day(), 20, 'Should not update if its less the correct number of days' );
+
+               // Test control.convertHourToTwentyFourHourFormat().
+               assert.equal( control.convertHourToTwentyFourHourFormat( 11, 'pm' ), 23 );
+               assert.equal( control.convertHourToTwentyFourHourFormat( 12, 'pm' ), 12 );
+               assert.equal( control.convertHourToTwentyFourHourFormat( 12, 'am' ), 0 );
+               assert.equal( control.convertHourToTwentyFourHourFormat( 11, 'am' ), 11 );
+
+               // Test control.toggleFutureDateNotification().
+               assert.deepEqual( control.toggleFutureDateNotification(), control );
+               control.toggleFutureDateNotification( true );
+               assert.ok( control.notifications.has( 'not_future_date' ) );
+               control.toggleFutureDateNotification( false );
+               assert.notOk( control.notifications.has( 'not_future_date' ) );
+
+               // Test control.populateDateInputs();
+               control.populateDateInputs();
+               control.dateInputs.each( function() {
+                       var node = jQuery( this );
+                   assert.equal( node.val(), control.inputElements[ node.data( 'component' ) ].get() );
+               } );
+
+               // Test control.validateInputs();
+               hour( 33 );
+               assert.ok( control.validateInputs() );
+               hour( 10 );
+               assert.notOk( control.validateInputs() );
+               minute( 123 );
+               assert.ok( control.validateInputs() );
+               minute( 20 );
+               assert.notOk( control.validateInputs() );
+
+               // Test control.populateSetting();
+               day( 2 );
+               month( 11 );
+               year( 2018 );
+               hour( 4 );
+               minute( 20 );
+               ampm( 'pm' );
+               control.populateSetting();
+               assert.equal( control.setting(), '2018-11-02 16:20:00' );
+
+               hour( 123 );
+               control.populateSetting();
+               assert.equal( control.setting(), '2018-11-02 16:20:00' ); // Should not update if invalid hour.
+
+               hour( 5 );
+               control.populateSetting();
+               assert.equal( control.setting(), '2018-11-02 17:20:00' );
+
+               // Test control.isFutureDate();
+               day( 2 );
+               month( 11 );
+               year( 2318 );
+               hour( 4 );
+               minute( 20 );
+               ampm( 'pm' );
+               assert.ok( control.isFutureDate() );
+
+               year( 2016 );
+               assert.notOk( control.isFutureDate() );
+
+               /**
+                * Test control.updateMinutesForHour().
+                * Run this at the end or else the above tests may fail.
+                */
+               hour( 24 );
+               minute( 32 );
+               control.inputElements.ampm = false; // Because it works only when the time is twenty four hour format.
+               control.updateMinutesForHour();
+               assert.deepEqual( minute(), 0 );
+
+               // Tear Down.
+               wp.customize.control.remove( controlId );
+       });
+
+       module( 'Customize Sections: wp.customize.OuterSection' );
+       test( 'Test OuterSection', function( assert ) {
+               var section, sectionId = 'test_outer_section', body = jQuery( 'body' ),
+                       defaultSection, defaultSectionId = 'fixture-section';
+
+               defaultSection = wp.customize.section( defaultSectionId );
+
+               section = new wp.customize.OuterSection( sectionId, {
+                       params: {
+                               content: defaultSection.params.content,
+                               type: 'outer'
+                       }
+               } );
+
+               wp.customize.section.add( sectionId, section );
+               wp.customize.section.add( defaultSectionId, section );
+
+               assert.equal( section.containerPaneParent, '.customize-outer-pane-parent' );
+               assert.equal( section.containerParent.selector, '#customize-outer-theme-controls' );
+
+               defaultSection.expand();
+               section.expand();
+               assert.ok( body.hasClass( 'outer-section-open' ) );
+               assert.ok( section.container.hasClass( 'open' ) );
+               assert.ok( defaultSection.expanded() ); // Ensure it does not affect other sections state.
+
+               section.collapse();
+               assert.notOk( body.hasClass( 'outer-section-open' ) );
+               assert.notOk( section.container.hasClass( 'open' ) ); // Ensure it does not affect other sections state.
+               assert.ok( defaultSection.expanded() );
+
+               // Tear down
+               wp.customize.section.remove( sectionId );
+       });
+
+       module( 'Customize Controls: PreviewLinkControl' );
+       test( 'Test PreviewLinkControl creation and its methods', function( assert ) {
+               var section, sectionId = 'publish_settings', newLink;
+
+               section = wp.customize.section( sectionId );
+               section.deferred.embedded.resolve();
+
+               assert.expect( 9 );
+               section.deferred.embedded.done( function() {
+                       _.each( section.controls(), function( control ) {
+                               if ( 'changeset_preview_link' === control.id ) {
+                                       assert.equal( control.templateSelector, 'customize-preview-link-control' );
+                                       assert.equal( _.size( control.previewElements ), control.elements.length );
+
+                                       // Test control.ready().
+                                       newLink = 'http://example.org?' + wp.customize.settings.changeset.uuid;
+                                       control.setting.set( newLink );
+
+                                       assert.equal( control.previewElements.input(), newLink );
+                                       assert.equal( control.previewElements.link(), newLink );
+                                       assert.equal( control.previewElements.link.element.attr( 'href' ), newLink );
+                                       assert.equal( control.previewElements.link.element.attr( 'target' ), wp.customize.settings.changeset.uuid );
+
+                                       // Test control.toggleSaveNotification().
+                                       control.toggleSaveNotification( true );
+                                       assert.ok( control.notifications.has( 'changes_not_saved' ) );
+                                       control.toggleSaveNotification( false );
+                                       assert.notOk( control.notifications.has( 'changes_not_saved' ) );
+
+                                       // Test control.updatePreviewLink().
+                                       control.updatePreviewLink();
+                                       assert.equal( control.setting.get(), wp.customize.previewer.getFrontendPreviewUrl() );
+                               }
+                       } );
+               } );
+       });
</ins><span class="cx" style="display: block; padding: 0 10px"> });
</span></span></pre>
</div>
</div>

</body>
</html>