<!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>[27419] trunk/src: Add widget management to the customizer.</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">
<dt>Revision</dt> <dd><a href="http://core.trac.wordpress.org/changeset/27419">27419</a></dd>
<dt>Author</dt> <dd>nacin</dd>
<dt>Date</dt> <dd>2014-03-05 20:40:36 +0000 (Wed, 05 Mar 2014)</dd>
</dl>
<h3>Log Message</h3>
<pre>Add widget management to the customizer.
This brings in the Widget Customizer plugin: https://wordpress.org/plugins/widget-customizer/.
props westonruter, shaunandrews, michael-arestad, johnregan3, akeda, topher1kenobe, topquarky, bobbravo2, ricardocorreia. And for good measure, props westonruter.
see <a href="http://core.trac.wordpress.org/ticket/27112">#27112</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminadminajaxphp">trunk/src/wp-admin/admin-ajax.php</a></li>
<li><a href="#trunksrcwpadminincludesajaxactionsphp">trunk/src/wp-admin/includes/ajax-actions.php</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>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsscustomizewidgetscss">trunk/src/wp-admin/css/customize-widgets.css</a></li>
<li><a href="#trunksrcwpadminjscustomizewidgetsjs">trunk/src/wp-admin/js/customize-widgets.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizewidgetsphp">trunk/src/wp-includes/class-wp-customize-widgets.php</a></li>
<li><a href="#trunksrcwpincludesjscustomizepreviewwidgetsjs">trunk/src/wp-includes/js/customize-preview-widgets.js</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminadminajaxphp"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-admin/admin-ajax.php (27418 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-admin/admin-ajax.php 2014-03-05 20:03:33 UTC (rev 27418)
+++ trunk/src/wp-admin/admin-ajax.php 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -58,7 +58,7 @@
</span><span class="cx"> 'wp-remove-post-lock', 'dismiss-wp-pointer', 'upload-attachment', 'get-attachment',
</span><span class="cx"> 'query-attachments', 'save-attachment', 'save-attachment-compat', 'send-link-to-editor',
</span><span class="cx"> 'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs',
</span><del>- 'save-user-color-scheme',
</del><ins>+ 'save-user-color-scheme', 'update-widget',
</ins><span class="cx"> );
</span><span class="cx">
</span><span class="cx"> // Register core Ajax calls.
</span></span></pre></div>
<a id="trunksrcwpadmincsscustomizewidgetscss"></a>
<div class="addfile"><h4>Added: trunk/src/wp-admin/css/customize-widgets.css (0 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-admin/css/customize-widgets.css (rev 0)
+++ trunk/src/wp-admin/css/customize-widgets.css 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -0,0 +1,623 @@
</span><ins>+.wp-full-overlay-sidebar {
+ overflow: visible;
+}
+
+/**
+ * Hide all sidebar sections by default, only show them (via JS) once the
+ * preview loads and we know whether the sidebars are used in the template.
+ */
+
+.control-section[id^="accordion-section-sidebar-widgets-"],
+.customize-control-sidebar_widgets label,
+.customize-control-sidebar_widgets .hide-if-js {
+ /* The link in .customize-control-sidebar_widgets .hide-if-js will fail if it ever gets used. */
+ display:none;
+}
+
+.customize-control-widget_form .widget-top {
+ -webkit-transition: opacity 0.5s;
+ transition: opacity 0.5s;
+}
+
+.customize-control-widget_form:not(.widget-rendered) .widget-top {
+ opacity: 0.5;
+}
+
+
+.customize-control-widget_form .widget-control-save {
+ display: none;
+}
+
+.customize-control-widget_form .spinner {
+ display: inline;
+ opacity: 0.0;
+ -webkit-transition: opacity 0.1s;
+ transition: opacity 0.1s;
+}
+.customize-control-widget_form.previewer-loading .spinner {
+ opacity: 1.0;
+}
+
+.customize-control-widget_form .widget {
+ margin-bottom: 0;
+}
+
+.customize-control-widget_form:not(.wide-widget-control) {
+ /**
+ * Prevent plugins (e.g. Widget Visibility in Jetpack) from forcing widget forms
+ * to be wide and so overflow the customizer panel
+ */
+ left: auto !important;
+ max-width: 100%;
+}
+.customize-control-widget_form.wide-widget-control .widget-inside {
+ position: fixed;
+ left: 299px;
+ top: 25%;
+ padding: 20px;
+ border: 1px solid rgb(229, 229, 229);
+ z-index: -1;
+}
+.customize-control-widget_form.wide-widget-control.collapsing .widget-inside {
+ z-index: -2;
+}
+
+.customize-control-widget_form.wide-widget-control .widget-top {
+ -webkit-transition: background-color 0.4s;
+ transition: background-color 0.4s;
+}
+.customize-control-widget_form.wide-widget-control.expanding .widget-top,
+.customize-control-widget_form.wide-widget-control.expanded:not(.collapsing) .widget-top {
+ background-color: rgb(227, 227, 227);
+}
+
+.widget-inside {
+ padding: 1px 10px 10px 10px;
+ border-top: none;
+ line-height: 16px;
+}
+
+.widget-top {
+ cursor: move;
+}
+
+.customize-control-widget_form.expanded a.widget-action:after {
+ content: "\f142";
+}
+
+.customize-control-widget_form.wide-widget-control a.widget-action:after {
+ content: "\f139";
+}
+
+.customize-control-widget_form.wide-widget-control.expanded a.widget-action:after {
+ content: "\f141";
+}
+
+.widget-title-action {
+ cursor: pointer;
+}
+
+.customize-control-widget_form .widget .customize-control-title {
+ cursor: move;
+}
+
+/* @todo What does this do? */
+.control-section.accordion-section.widget-customizer-highlighted > .accordion-section-title,
+.customize-control-widget_form.widget-customizer-highlighted {
+ outline: none;
+ -webkit-box-shadow: 0 0 3px #ce0000;
+ box-shadow: 0 0 3px #ce0000;
+}
+
+#widget-customizer-control-templates {
+ display: none;
+}
+
+
+/* MP6-compat */
+#customize-theme-controls .accordion-section-content .widget {
+ color: black;
+}
+
+
+/**
+* Widget reordering styles
+**/
+
+.reorder-toggle {
+ float: right;
+ padding: 5px 10px;
+ margin-right: 10px;
+ text-decoration: none;
+ cursor: pointer;
+ outline: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.reorder-toggle:focus {
+ outline: 1px dotted;
+}
+
+.reorder-done,
+.reordering .reorder {
+ display: none;
+}
+
+.reordering .reorder-done {
+ display: block;
+ color: #aa0000;
+}
+
+#customize-theme-controls .reordering .add-new-widget {
+ opacity: 0.2;
+ pointer-events: none;
+ cursor: not-allowed;
+}
+
+#customize-theme-controls .widget-reorder-nav {
+ display: none;
+ float: right;
+ background-color: #fafafa;
+}
+
+.widget-reorder-nav span {
+ position: relative;
+ overflow: hidden;
+ float: left;
+ display: block;
+ width: 33px; /* was 42px for mobile */
+ height: 43px;
+ color: #888;
+ text-indent: -9999px;
+ cursor: pointer;
+ outline: none;
+}
+
+.widget-reorder-nav span:before {
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ font: normal normal 20px/43px 'Genericons';
+ text-align: center;
+ text-indent: 0;
+}
+
+.widget-reorder-nav span:hover,
+.widget-reorder-nav span:focus {
+ color: #444;
+ background: #eee;
+}
+
+.move-widget:before {
+ content: '\f442';
+}
+
+.move-widget-down:before {
+ content: '\f431';
+}
+
+.move-widget-up:before {
+ content: '\f432';
+}
+
+#customize-theme-controls .first-widget .move-widget-up,
+#customize-theme-controls .last-widget .move-widget-down {
+ color: #d5d5d5;
+ cursor: default;
+}
+
+#customize-theme-controls .move-widget-area {
+ display: none;
+ background: #fff;
+ border: 1px solid #dedede;
+ border-top: none;
+ cursor: auto;
+}
+
+#customize-theme-controls .reordering .move-widget-area.active {
+ display: block;
+}
+
+#customize-theme-controls .move-widget-area .description {
+ margin: 0;
+ padding: 15px 20px;
+ font-weight: 400;
+}
+
+#customize-theme-controls .widget-area-select {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+#customize-theme-controls .widget-area-select li {
+ position: relative;
+ margin: 0;
+ padding: 13px 15px 15px 42px;
+ color: #555;
+ border-top: 1px solid #eee;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+#customize-theme-controls .widget-area-select li:before {
+ display: none;
+ content: '\f418';
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ font-family: 'Genericons';
+ font-size: 24px;
+ line-height: 1;
+}
+
+#customize-theme-controls .widget-area-select li:last-child {
+ border-bottom: 1px solid #eee;
+}
+
+#customize-theme-controls .widget-area-select .selected {
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0,0,0,.4);
+ border-top: 1px solid #207fa1;
+ background: #2ea2cc;
+}
+
+#customize-theme-controls .widget-area-select .selected:before {
+ display: block;
+}
+
+#customize-theme-controls .widget-area-select .selected:last-child {
+ border-bottom: 1px solid #207fa1;
+}
+
+#customize-theme-controls .move-widget-actions {
+ text-align: right;
+ padding: 12px;
+}
+
+#customize-theme-controls .widget-area-select + li {
+ border-top: 1px solid #207fa1;
+}
+
+#customize-theme-controls .reordering .widget-title-action {
+ display: none;
+}
+
+#customize-theme-controls .reordering .widget-reorder-nav {
+ display: block;
+}
+
+
+/**
+ * Styles for new widget addition panel
+ */
+.wp-full-overlay-main {
+ right: auto; /* this overrides a right: 0; which causes the preview to resize, I'd rather have it go off screen at the normal size. */
+ width: 100%;
+}
+
+.add-new-widget {
+ cursor: pointer;
+ float: right;
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -moz-outline: none;
+ outline: none;
+}
+
+.add-new-widget:before {
+ content: "\f132";
+ display: inline-block;
+ position: relative;
+ left: -2px;
+ top: -1px;
+ font: normal 16px/1 'dashicons';
+ vertical-align: middle;
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+ -webkit-font-smoothing: antialiased;
+}
+
+body.adding-widget .add-new-widget,
+body.adding-widget .add-new-widget:hover {
+ background: #EEE;
+ border-color: #999;
+ color: #333;
+ -webkit-box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
+ box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
+}
+body.adding-widget .add-new-widget:before {
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+
+#available-widgets .widget {
+ position: static;
+}
+
+/* override widgets admin page rules in wp-admin/css/wp-admin.css */
+#widgets-left #available-widgets .widget {
+ float: none !important;
+ width: auto !important;
+}
+
+#available-widgets {
+ position: absolute;
+ overflow: auto;
+ top: 0;
+ bottom: 0;
+ left: -301px;
+ width: 300px;
+ margin: 0;
+ z-index: 1;
+ background: #fff;
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+ border-right: 1px solid #dddddd;
+}
+
+#available-widgets-filter {
+ padding: 8px 17px 7px 13px;
+ border-bottom: 1px solid #e4e4e4;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+#available-widgets-filter input {
+ padding: 5px 10px 2px 10px;
+ width: 100%;
+}
+
+#available-widgets .widget-tpl {
+ position: relative;
+ padding: 20px 15px 20px 60px;
+ border-bottom: 1px solid #e4e4e4;
+ cursor: pointer;
+}
+
+#available-widgets .widget-tpl:hover,
+#available-widgets .widget-tpl.selected {
+ background: #fafafa;
+}
+
+#available-widgets .widget-top,
+#available-widgets .widget-top:hover {
+ border: none;
+ background: transparent;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+#available-widgets .widget-title h4 {
+ padding: 0 0 5px;
+ font-size: 14px;
+}
+
+#available-widgets .widget .widget-description {
+ padding: 0;
+ color: #777;
+}
+
+#customize-preview {
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+}
+
+body.adding-widget #available-widgets {
+ left: 0;
+}
+
+body.adding-widget .wp-full-overlay-main {
+ left: 300px;
+}
+
+body.adding-widget #customize-preview {
+ opacity: 0.4;
+}
+
+
+/** Widget Icon styling **
+
+* No plurals in naming.
+* Ordered from lowest to highest specificity.
+
+**/
+#available-widgets .widget-title {
+ position: relative;
+}
+
+#available-widgets .widget-title:before {
+ content:"\f132";
+ position: absolute;
+ top: -3px;
+ right: 100%;
+ margin-right: 20px;
+ width: 20px;
+ height: 20px;
+ color: #333;
+ font: normal 20px/1 'dashicons', 'widgeticons';
+ text-align: center;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* smiley */
+#available-widgets [class*="easy"] .widget-title:before { content: "\f328"; top: -4px; }
+
+/* star-filled */
+#available-widgets [class*="super"] .widget-title:before,
+#available-widgets [class*="like"] .widget-title:before { content: "\f155"; top: -4px; }
+
+/* wordpress */
+#available-widgets [class*="meta"] .widget-title:before { content: "\f120"; }
+
+/* archive-box */
+#available-widgets [class*="archives"] .widget-title:before { content: "\f483"; top: -4px; }
+
+/* category */
+#available-widgets [class*="categor"] .widget-title:before { content: "\f318"; top: -4px; }
+
+/* comments */
+#available-widgets [class*="comment"] .widget-title:before,
+#available-widgets [class*="testimonial"] .widget-title:before,
+#available-widgets [class*="chat"] .widget-title:before { content: "\f101"; }
+
+/* post */
+#available-widgets [class*="post"] .widget-title:before { content: "\f109"; }
+
+/* admin-page */
+#available-widgets [class*="page"] .widget-title:before { content: "\f105"; }
+
+/* text */
+#available-widgets [class*="text"] .widget-title:before { content: "\f480"; }
+
+/* links */
+#available-widgets [class*="link"] .widget-title:before { content: "\f103"; }
+
+/* search */
+#available-widgets [class*="search"] .widget-title:before { content: "\f179"; }
+
+/* menu */
+#available-widgets [class*="menu"] .widget-title:before,
+#available-widgets [class*="nav"] .widget-title:before { content: "\f333"; }
+
+/* tag-cloud */
+#available-widgets [class*="tag"] .widget-title:before { content: "\f481"; }
+
+/* rss */
+#available-widgets [class*="rss"] .widget-title:before { content: "\f303"; top: -6px; }
+
+/* calendar */
+#available-widgets [class*="event"] .widget-title:before,
+#available-widgets [class*="calendar"] .widget-title:before { content: "\f145"; top: -4px;}
+
+/* format-image */
+#available-widgets [class*="image"] .widget-title:before,
+#available-widgets [class*="photo"] .widget-title:before,
+#available-widgets [class*="slide"] .widget-title:before,
+#available-widgets [class*="instagram"] .widget-title:before { content: "\f128"; }
+
+/* format-gallery */
+#available-widgets [class*="album"] .widget-title:before,
+#available-widgets [class*="galler"] .widget-title:before { content: "\f161"; }
+
+/* format-video */
+#available-widgets [class*="video"] .widget-title:before,
+#available-widgets [class*="tube"] .widget-title:before { content: "\f126"; }
+
+/* format-audio */
+#available-widgets [class*="music"] .widget-title:before,
+#available-widgets [class*="radio"] .widget-title:before,
+#available-widgets [class*="audio"] .widget-title:before { content: "\f127"; }
+
+/* admin-users */
+#available-widgets [class*="login"] .widget-title:before,
+#available-widgets [class*="user"] .widget-title:before,
+#available-widgets [class*="member"] .widget-title:before,
+#available-widgets [class*="avatar"] .widget-title:before,
+#available-widgets [class*="subscriber"] .widget-title:before,
+#available-widgets [class*="profile"] .widget-title:before,
+#available-widgets [class*="grofile"] .widget-title:before { content: "\f110"; }
+
+/* cart */
+#available-widgets [class*="commerce"] .widget-title:before,
+#available-widgets [class*="shop"] .widget-title:before,
+#available-widgets [class*="cart"] .widget-title:before { content: "\f174"; top: -4px; }
+
+/* shield */
+#available-widgets [class*="secur"] .widget-title:before,
+#available-widgets [class*="firewall"] .widget-title:before { content: "\f332"; }
+
+/* chart-bar */
+#available-widgets [class*="analytic"] .widget-title:before,
+#available-widgets [class*="stat"] .widget-title:before,
+#available-widgets [class*="poll"] .widget-title:before { content: "\f185"; }
+
+/* feedback */
+#available-widgets [class*="form"] .widget-title:before { content: "\f175"; }
+
+/* email-alt */
+#available-widgets [class*="subscribe"] .widget-title:before,
+#available-widgets [class*="news"] .widget-title:before,
+#available-widgets [class*="contact"] .widget-title:before,
+#available-widgets [class*="mail"] .widget-title:before { content: "\f466"; }
+
+/* share */
+#available-widgets [class*="share"] .widget-title:before,
+#available-widgets [class*="socia"] .widget-title:before { content: "\f237"; }
+
+/* translation */
+#available-widgets [class*="lang"] .widget-title:before,
+#available-widgets [class*="translat"] .widget-title:before { content: "\f326"; }
+
+/* location-alt */
+#available-widgets [class*="locat"] .widget-title:before,
+#available-widgets [class*="map"] .widget-title:before { content: "\f231"; }
+
+/* download */
+#available-widgets [class*="download"] .widget-title:before { content: "\f316"; }
+
+/* cloud */
+#available-widgets [class*="weather"] .widget-title:before { content: "\f176"; top: -4px;}
+
+/* facebook */
+#available-widgets [class*="facebook"] .widget-title:before { content: "\f304"; }
+
+/* twitter */
+#available-widgets [class*="tweet"] .widget-title:before,
+#available-widgets [class*="twitter"] .widget-title:before { content: "\f301"; }
+
+
+@media screen and (max-height: 700px) and (min-width: 981px) {
+ .customize-control {
+ margin-bottom: 0;
+ }
+ .widget-top {
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ margin-top: -1px;
+ }
+ .widget-top:hover {
+ position: relative;
+ z-index: 1;
+ }
+ .last-widget {
+ margin-bottom: 15px;
+ }
+ .widget-title h4 {
+ padding: 13px 15px;
+ }
+ .widget-top a.widget-action:after {
+ padding-top: 9px;
+ }
+ .widget-reorder-nav span {
+ height: 39px;
+ }
+ .widget-reorder-nav span:before {
+ line-height: 39px;
+ }
+ #customize-theme-controls .widget-area-select li {
+ padding: 9px 15px 11px 42px;
+ }
+ #customize-theme-controls .widget-area-select li:before {
+ top: 6px;
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpadminincludesajaxactionsphp"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-admin/includes/ajax-actions.php (27418 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-admin/includes/ajax-actions.php 2014-03-05 20:03:33 UTC (rev 27418)
+++ trunk/src/wp-admin/includes/ajax-actions.php 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -1587,6 +1587,13 @@
</span><span class="cx"> wp_die();
</span><span class="cx"> }
</span><span class="cx">
</span><ins>+function wp_ajax_update_widget() {
+ require( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
+ $GLOBALS['wp_customize'] = new WP_Customize_Manager;
+
+ WP_Customize_Widgets::wp_ajax_update_widget();
+}
+
</ins><span class="cx"> function wp_ajax_upload_attachment() {
</span><span class="cx"> check_ajax_referer( 'media-form' );
</span><span class="cx">
</span></span></pre></div>
<a id="trunksrcwpadminjscustomizewidgetsjs"></a>
<div class="addfile"><h4>Added: trunk/src/wp-admin/js/customize-widgets.js (0 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-admin/js/customize-widgets.js (rev 0)
+++ trunk/src/wp-admin/js/customize-widgets.js 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -0,0 +1,1733 @@
</span><ins>+/*global wp, Backbone, _, jQuery, WidgetCustomizer_exports */
+/*exported WidgetCustomizer */
+var WidgetCustomizer = ( function ($) {
+ 'use strict';
+
+ var customize = wp.customize;
+ var self = {
+ update_widget_ajax_action: null,
+ update_widget_nonce_value: null,
+ update_widget_nonce_post_key: null,
+ i18n: {
+ save_btn_label: '',
+ save_btn_tooltip: '',
+ remove_btn_label: '',
+ remove_btn_tooltip: ''
+ },
+ available_widgets: [], // available widgets for instantiating
+ registered_widgets: [], // all widgets registered
+ active_sidebar_control: null,
+ previewer: null,
+ saved_widget_ids: {},
+ registered_sidebars: [],
+ tpl: {
+ move_widget_area: '',
+ widget_reorder_nav: ''
+ }
+ };
+ $.extend( self, WidgetCustomizer_exports );
+
+ // Lots of widgets expect this old ajaxurl global to be available
+ if ( typeof window.ajaxurl === 'undefined' ) {
+ window.ajaxurl = wp.ajax.settings.url;
+ }
+
+ // Unfortunately many widgets try to look for instances under div#widgets-right,
+ // so we have to add that ID to a container div in the customizer for compat
+ $( '#customize-theme-controls' ).closest( 'div:not([id])' ).attr( 'id', 'widgets-right' );
+
+ /**
+ * Set up model
+ */
+ var Widget = self.Widget = Backbone.Model.extend( {
+ id: null,
+ temp_id: null,
+ classname: null,
+ control_tpl: null,
+ description: null,
+ is_disabled: null,
+ is_multi: null,
+ multi_number: null,
+ name: null,
+ id_base: null,
+ transport: 'refresh',
+ params: [],
+ width: null,
+ height: null
+ } );
+ var WidgetCollection = self.WidgetCollection = Backbone.Collection.extend( {
+ model: Widget
+ } );
+ self.available_widgets = new WidgetCollection( self.available_widgets );
+
+ var Sidebar = self.Sidebar = Backbone.Model.extend( {
+ after_title: null,
+ after_widget: null,
+ before_title: null,
+ before_widget: null,
+ 'class': null,
+ description: null,
+ id: null,
+ name: null,
+ is_rendered: false
+ } );
+ var SidebarCollection = self.SidebarCollection = Backbone.Collection.extend( {
+ model: Sidebar
+ } );
+ self.registered_sidebars = new SidebarCollection( self.registered_sidebars );
+
+ /**
+ * On DOM ready, initialize some meta functionality independent of specific
+ * customizer controls.
+ */
+ self.init = function () {
+ this.showFirstSidebarIfRequested();
+ this.availableWidgetsPanel.setup();
+ };
+ wp.customize.bind( 'ready', function () {
+ self.init();
+ } );
+
+ /**
+ * Listen for updates to which sidebars are rendered in the preview and toggle
+ * the customizer sections accordingly.
+ */
+ self.showFirstSidebarIfRequested = function () {
+ if ( ! /widget-customizer=open/.test( location.search ) ) {
+ return;
+ }
+
+ var show_first_visible_sidebar = function () {
+ self.registered_sidebars.off( 'change:is_rendered', show_first_visible_sidebar );
+ var first_rendered_sidebar = self.registered_sidebars.find( function ( sidebar ) {
+ return sidebar.get( 'is_rendered' );
+ } );
+ if ( ! first_rendered_sidebar ) {
+ return;
+ }
+ var section = $( '#accordion-section-sidebar-widgets-' + first_rendered_sidebar.get( 'id' ) );
+ if ( ! section.hasClass( 'open' ) ) {
+ section.find( '.accordion-section-title' ).trigger( 'click' );
+ }
+ section[0].scrollIntoView();
+ };
+ show_first_visible_sidebar = _.debounce( show_first_visible_sidebar, 100 ); // so only fires when all updated at end
+ self.registered_sidebars.on( 'change:is_rendered', show_first_visible_sidebar );
+ };
+
+ /**
+ * Sidebar Widgets control
+ * Note that 'sidebar_widgets' must match the Sidebar_Widgets_WP_Customize_Control::$type
+ */
+ customize.controlConstructor.sidebar_widgets = customize.Control.extend( {
+
+ /**
+ * Set up the control
+ */
+ ready: function() {
+ var control = this;
+ control.control_section = control.container.closest( '.control-section' );
+ control.section_content = control.container.closest( '.accordion-section-content' );
+ control._setupModel();
+ control._setupSortable();
+ control._setupAddition();
+ control._applyCardinalOrderClassNames();
+ },
+
+ /**
+ * Update ordering of widget control forms when the setting is updated
+ */
+ _setupModel: function() {
+ var control = this;
+ var registered_sidebar = self.registered_sidebars.get( control.params.sidebar_id );
+
+ control.setting.bind( function( new_widget_ids, old_widget_ids ) {
+ var removed_widget_ids = _( old_widget_ids ).difference( new_widget_ids );
+
+ // Filter out any persistent widget_ids for widgets which have been deactivated
+ new_widget_ids = _( new_widget_ids ).filter( function ( new_widget_id ) {
+ var parsed_widget_id = parse_widget_id( new_widget_id );
+ return !! self.available_widgets.findWhere( { id_base: parsed_widget_id.id_base } );
+ } );
+
+ var widget_form_controls = _( new_widget_ids ).map( function ( widget_id ) {
+ var widget_form_control = self.getWidgetFormControlForWidget( widget_id );
+ if ( ! widget_form_control ) {
+ widget_form_control = control.addWidget( widget_id );
+ }
+ return widget_form_control;
+ } );
+
+ // Sort widget controls to their new positions
+ widget_form_controls.sort( function ( a, b ) {
+ var a_index = new_widget_ids.indexOf( a.params.widget_id );
+ var b_index = new_widget_ids.indexOf( b.params.widget_id );
+ if ( a_index === b_index ) {
+ return 0;
+ }
+ return a_index < b_index ? -1 : 1;
+ } );
+
+ var sidebar_widgets_add_control = control.section_content.find( '.customize-control-sidebar_widgets' );
+
+ // Append the controls to put them in the right order
+ var final_control_containers = _( widget_form_controls ).map( function( widget_form_controls ) {
+ return widget_form_controls.container[0];
+ } );
+
+ // Re-sort widget form controls (including widgets form other sidebars newly moved here)
+ sidebar_widgets_add_control.before( final_control_containers );
+ control._applyCardinalOrderClassNames();
+
+ // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated
+ _( widget_form_controls ).each( function ( widget_form_control ) {
+ widget_form_control.params.sidebar_id = control.params.sidebar_id;
+ } );
+
+ // Cleanup after widget removal
+ _( removed_widget_ids ).each( function ( removed_widget_id ) {
+
+ // Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update
+ setTimeout( function () {
+ var is_present_in_another_sidebar = false;
+
+ // Check if the widget is in another sidebar
+ wp.customize.each( function ( other_setting ) {
+ if ( other_setting.id === control.setting.id || 0 !== other_setting.id.indexOf( 'sidebars_widgets[' ) || other_setting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
+ return;
+ }
+ var other_sidebar_widgets = other_setting();
+ var i = other_sidebar_widgets.indexOf( removed_widget_id );
+ if ( -1 !== i ) {
+ is_present_in_another_sidebar = true;
+ }
+ } );
+
+ // If the widget is present in another sidebar, abort!
+ if ( is_present_in_another_sidebar ) {
+ return;
+ }
+
+ var removed_control = self.getWidgetFormControlForWidget( removed_widget_id );
+
+ // Detect if widget control was dragged to another sidebar
+ var was_dragged_to_another_sidebar = (
+ removed_control &&
+ $.contains( document, removed_control.container[0] ) &&
+ ! $.contains( control.section_content[0], removed_control.container[0] )
+ );
+
+ // Delete any widget form controls for removed widgets
+ if ( removed_control && ! was_dragged_to_another_sidebar ) {
+ wp.customize.control.remove( removed_control.id );
+ removed_control.container.remove();
+ }
+
+ // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
+ // This prevents the inactive widgets sidebar from overflowing with throwaway widgets
+ if ( self.saved_widget_ids[removed_widget_id] ) {
+ var inactive_widgets = wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
+ inactive_widgets.push( removed_widget_id );
+ wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactive_widgets ).unique() );
+ }
+
+ // Make old single widget available for adding again
+ var removed_id_base = parse_widget_id( removed_widget_id ).id_base;
+ var widget = self.available_widgets.findWhere( { id_base: removed_id_base } );
+ if ( widget && ! widget.get( 'is_multi' ) ) {
+ widget.set( 'is_disabled', false );
+ }
+ } );
+
+ } );
+ } );
+
+ // Update the model with whether or not the sidebar is rendered
+ self.previewer.bind( 'rendered-sidebars', function ( rendered_sidebars ) {
+ var is_rendered = !! rendered_sidebars[control.params.sidebar_id];
+ registered_sidebar.set( 'is_rendered', is_rendered );
+ } );
+
+ // Show the sidebar section when it becomes visible
+ registered_sidebar.on( 'change:is_rendered', function ( ) {
+ var section_selector = '#accordion-section-sidebar-widgets-' + this.get( 'id' );
+ var section = $( section_selector );
+ if ( this.get( 'is_rendered' ) ) {
+ section.stop().slideDown( function () {
+ $( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
+ } );
+ } else {
+ // Make sure that hidden sections get closed first
+ if ( section.hasClass( 'open' ) ) {
+ // it would be nice if accordionSwitch() in accordion.js was public
+ section.find( '.accordion-section-title' ).trigger( 'click' );
+ }
+ section.stop().slideUp();
+ }
+ } );
+ },
+
+ /**
+ * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
+ */
+ _setupSortable: function () {
+ var control = this;
+ control.is_reordering = false;
+
+ /**
+ * Update widget order setting when controls are re-ordered
+ */
+ control.section_content.sortable( {
+ items: '> .customize-control-widget_form',
+ handle: '.widget-top',
+ axis: 'y',
+ connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
+ update: function () {
+ var widget_container_ids = control.section_content.sortable( 'toArray' );
+ var widget_ids = $.map( widget_container_ids, function ( widget_container_id ) {
+ return $( '#' + widget_container_id ).find( ':input[name=widget-id]' ).val();
+ } );
+ control.setting( widget_ids );
+ }
+ } );
+
+ /**
+ * Expand other customizer sidebar section when dragging a control widget over it,
+ * allowing the control to be dropped into another section
+ */
+ control.control_section.find( '.accordion-section-title' ).droppable( {
+ accept: '.customize-control-widget_form',
+ over: function () {
+ if ( ! control.control_section.hasClass( 'open' ) ) {
+ control.control_section.addClass( 'open' );
+ control.section_content.toggle( false ).slideToggle( 150, function () {
+ control.section_content.sortable( 'refreshPositions' );
+ } );
+ }
+ }
+ } );
+
+ /**
+ * Keyboard-accessible reordering
+ */
+ control.container.find( '.reorder-toggle' ).on( 'click keydown', function( event ) {
+ if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
+ return;
+ }
+
+ control.toggleReordering( ! control.is_reordering );
+ } );
+ },
+
+ /**
+ * Set up UI for adding a new widget
+ */
+ _setupAddition: function () {
+ var control = this;
+
+ control.container.find( '.add-new-widget' ).on( 'click keydown', function( event ) {
+ if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
+ return;
+ }
+
+ if ( control.section_content.hasClass( 'reordering' ) ) {
+ return;
+ }
+
+ // @todo Use an control.is_adding state
+ if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
+ self.availableWidgetsPanel.open( control );
+ } else {
+ self.availableWidgetsPanel.close();
+ }
+ } );
+ },
+
+ /**
+ * Add classes to the widget_form controls to assist with styling
+ */
+ _applyCardinalOrderClassNames: function () {
+ var control = this;
+ control.section_content.find( '.customize-control-widget_form' )
+ .removeClass( 'first-widget' )
+ .removeClass( 'last-widget' )
+ .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
+
+ control.section_content.find( '.customize-control-widget_form:first' )
+ .addClass( 'first-widget' )
+ .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
+ control.section_content.find( '.customize-control-widget_form:last' )
+ .addClass( 'last-widget' )
+ .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
+ },
+
+
+ /***********************************************************************
+ * Begin public API methods
+ **********************************************************************/
+
+ /**
+ * Enable/disable the reordering UI
+ *
+ * @param {Boolean} toggle to enable/disable reordering
+ */
+ toggleReordering: function ( toggle ) {
+ var control = this;
+ toggle = Boolean( toggle );
+ if ( toggle === control.section_content.hasClass( 'reordering' ) ) {
+ return;
+ }
+
+ control.is_reordering = toggle;
+ control.section_content.toggleClass( 'reordering', toggle );
+
+ if ( toggle ) {
+ _( control.getWidgetFormControls() ).each( function ( form_control ) {
+ form_control.collapseForm();
+ } );
+ }
+ },
+
+ /**
+ * @return {wp.customize.controlConstructor.widget_form[]}
+ */
+ getWidgetFormControls: function () {
+ var control = this;
+ var form_controls = _( control.setting() ).map( function ( widget_id ) {
+ var setting_id = widget_id_to_setting_id( widget_id );
+ var form_control = customize.control( setting_id );
+ if ( ! form_control ) {
+ throw new Error( 'Unable to find widget_form control for ' + widget_id );
+ }
+ return form_control;
+ } );
+ return form_controls;
+ },
+
+ /**
+ * @param {string} widget_id or an id_base for adding a previously non-existing widget
+ * @returns {object} widget_form control instance
+ */
+ addWidget: function ( widget_id ) {
+ var control = this;
+ var parsed_widget_id = parse_widget_id( widget_id );
+ var widget_number = parsed_widget_id.number;
+ var widget_id_base = parsed_widget_id.id_base;
+ var widget = self.available_widgets.findWhere( {id_base: widget_id_base} );
+ if ( ! widget ) {
+ throw new Error( 'Widget unexpectedly not found.' );
+ }
+ if ( widget_number && ! widget.get( 'is_multi' ) ) {
+ throw new Error( 'Did not expect a widget number to be supplied for a non-multi widget' );
+ }
+
+ // Set up new multi widget
+ if ( widget.get( 'is_multi' ) && ! widget_number ) {
+ widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
+ widget_number = widget.get( 'multi_number' );
+ }
+
+ var control_html = $( '#widget-tpl-' + widget.get( 'id' ) ).html();
+ if ( widget.get( 'is_multi' ) ) {
+ control_html = control_html.replace( /<[^<>]+>/g, function ( m ) {
+ return m.replace( /__i__|%i%/g, widget_number );
+ } );
+ } else {
+ widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
+ }
+
+ var customize_control_type = 'widget_form';
+ var customize_control = $( '<li></li>' );
+ customize_control.addClass( 'customize-control' );
+ customize_control.addClass( 'customize-control-' + customize_control_type );
+ customize_control.append( $( control_html ) );
+ customize_control.find( '> .widget-icon' ).remove();
+ if ( widget.get( 'is_multi' ) ) {
+ customize_control.find( 'input[name="widget_number"]' ).val( widget_number );
+ customize_control.find( 'input[name="multi_number"]' ).val( widget_number );
+ }
+ widget_id = customize_control.find( '[name="widget-id"]' ).val();
+ customize_control.hide(); // to be slid-down below
+
+ var setting_id = 'widget_' + widget.get( 'id_base' );
+ if ( widget.get( 'is_multi' ) ) {
+ setting_id += '[' + widget_number + ']';
+ }
+ customize_control.attr( 'id', 'customize-control-' + setting_id.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
+
+ control.container.after( customize_control );
+
+ // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
+ var is_existing_widget = wp.customize.has( setting_id );
+ if ( ! is_existing_widget ) {
+ var setting_args = {
+ transport: 'refresh',
+ previewer: control.setting.previewer
+ };
+ wp.customize.create( setting_id, setting_id, {}, setting_args );
+ }
+
+ var Constructor = wp.customize.controlConstructor[customize_control_type];
+ var widget_form_control = new Constructor( setting_id, {
+ params: {
+ settings: {
+ 'default': setting_id
+ },
+ sidebar_id: control.params.sidebar_id,
+ widget_id: widget_id,
+ widget_id_base: widget.get( 'id_base' ),
+ type: customize_control_type,
+ is_new: ! is_existing_widget,
+ width: widget.get( 'width' ),
+ height: widget.get( 'height' ),
+ is_wide: widget.get( 'is_wide' )
+ },
+ previewer: control.setting.previewer
+ } );
+ wp.customize.control.add( setting_id, widget_form_control );
+
+ // Make sure widget is removed from the other sidebars
+ wp.customize.each( function ( other_setting ) {
+ if ( other_setting.id === control.setting.id ) {
+ return;
+ }
+ if ( 0 !== other_setting.id.indexOf( 'sidebars_widgets[' ) ) {
+ return;
+ }
+ var other_sidebar_widgets = other_setting().slice();
+ var i = other_sidebar_widgets.indexOf( widget_id );
+ if ( -1 !== i ) {
+ other_sidebar_widgets.splice( i );
+ other_setting( other_sidebar_widgets );
+ }
+ } );
+
+ // Add widget to this sidebar
+ var sidebar_widgets = control.setting().slice();
+ if ( -1 === sidebar_widgets.indexOf( widget_id ) ) {
+ sidebar_widgets.push( widget_id );
+ control.setting( sidebar_widgets );
+ }
+
+ customize_control.slideDown( function () {
+ if ( is_existing_widget ) {
+ widget_form_control.expandForm();
+ widget_form_control.updateWidget( {
+ instance: widget_form_control.setting(),
+ complete: function ( error ) {
+ if ( error ) {
+ throw error;
+ }
+ widget_form_control.focus();
+ }
+ } );
+ } else {
+ widget_form_control.focus();
+ }
+ } );
+
+ return widget_form_control;
+ }
+
+ } );
+
+ /**
+ * Widget Form control
+ * Note that 'widget_form' must match the Widget_Form_WP_Customize_Control::$type
+ */
+ customize.controlConstructor.widget_form = customize.Control.extend( {
+
+ /**
+ * Set up the control
+ */
+ ready: function() {
+ var control = this;
+ control._setupModel();
+ control._setupWideWidget();
+ control._setupControlToggle();
+ control._setupWidgetTitle();
+ control._setupReorderUI();
+ control._setupHighlightEffects();
+ control._setupUpdateUI();
+ control._setupRemoveUI();
+ control.hook( 'init' );
+ },
+
+ /**
+ * Hooks for widgets to support living in the customizer control
+ */
+ hooks: {
+ _default: {},
+ rss: {
+ formUpdated: function ( serialized_form ) {
+ var control = this;
+ var old_widget_error = control.container.find( '.widget-error:first' );
+ var new_widget_error = serialized_form.find( '.widget-error:first' );
+ if ( old_widget_error.length && new_widget_error.length ) {
+ old_widget_error.replaceWith( new_widget_error );
+ } else if ( old_widget_error.length ) {
+ old_widget_error.remove();
+ } else if ( new_widget_error.length ) {
+ control.container.find( '.widget-content' ).prepend( new_widget_error );
+ }
+ }
+ }
+ },
+
+ /**
+ * Trigger an 'action' which a specific widget type can handle
+ *
+ * @param name
+ */
+ hook: function ( name ) {
+ var args = Array.prototype.slice.call( arguments, 1 );
+ var handler;
+ if ( this.hooks[this.params.widget_id_base] && this.hooks[this.params.widget_id_base][name] ) {
+ handler = this.hooks[this.params.widget_id_base][name];
+ } else if ( this.hooks._default[name] ) {
+ handler = this.hooks._default[name];
+ }
+ if ( handler ) {
+ handler.apply( this, args );
+ }
+ },
+
+ /**
+ * Handle changes to the setting
+ */
+ _setupModel: function () {
+ var control = this;
+
+ // Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
+ var remember_saved_widget_id = function () {
+ self.saved_widget_ids[control.params.widget_id] = true;
+ };
+ wp.customize.bind( 'ready', remember_saved_widget_id );
+ wp.customize.bind( 'saved', remember_saved_widget_id );
+
+ control._update_count = 0;
+ control.is_widget_updating = false;
+
+ // Update widget whenever model changes
+ control.setting.bind( function( to, from ) {
+ if ( ! _( from ).isEqual( to ) && ! control.is_widget_updating ) {
+ control.updateWidget( { instance: to } );
+ }
+ } );
+ },
+
+ /**
+ * Add special behaviors for wide widget controls
+ */
+ _setupWideWidget: function () {
+ var control = this;
+ if ( ! control.params.is_wide ) {
+ return;
+ }
+ var widget_inside = control.container.find( '.widget-inside' );
+ var customize_sidebar = $( '.wp-full-overlay-sidebar-content:first' );
+ control.container.addClass( 'wide-widget-control' );
+
+ control.container.find( '.widget-content:first' ).css( {
+ 'min-width': control.params.width,
+ 'min-height': control.params.height
+ } );
+
+ /**
+ * Keep the widget-inside positioned so the top of fixed-positioned
+ * element is at the same top position as the widget-top. When the
+ * widget-top is scrolled out of view, keep the widget-top in view;
+ * likewise, don't allow the widget to drop off the bottom of the window.
+ */
+ var position_widget = function () {
+ var offset_top = control.container.offset().top;
+ var height = widget_inside.outerHeight();
+ var top = Math.max( offset_top, 0 );
+ var max_top = $( window ).height() - height;
+ top = Math.min( top, max_top );
+ widget_inside.css( 'top', top );
+ };
+
+ var theme_controls_container = $( '#customize-theme-controls' );
+ control.container.on( 'expand', function () {
+ customize_sidebar.on( 'scroll', position_widget );
+ $( window ).on( 'resize', position_widget );
+ theme_controls_container.on( 'expanded collapsed', position_widget );
+ position_widget();
+ } );
+ control.container.on( 'collapsed', function () {
+ customize_sidebar.off( 'scroll', position_widget );
+ theme_controls_container.off( 'expanded collapsed', position_widget );
+ $( window ).off( 'resize', position_widget );
+ } );
+
+ // Reposition whenever a sidebar's widgets are changed
+ wp.customize.each( function ( setting ) {
+ if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
+ setting.bind( function () {
+ if ( control.container.hasClass( 'expanded' ) ) {
+ position_widget();
+ }
+ } );
+ }
+ } );
+ },
+
+ /**
+ * Show/hide the control when clicking on the form title, when clicking
+ * the close button
+ */
+ _setupControlToggle: function() {
+ var control = this;
+ control.container.find( '.widget-top' ).on( 'click', function ( e ) {
+ e.preventDefault();
+ var sidebar_widgets_control = control.getSidebarWidgetsControl();
+ if ( sidebar_widgets_control.is_reordering ) {
+ return;
+ }
+ control.toggleForm();
+ } );
+
+ var close_btn = control.container.find( '.widget-control-close' );
+ // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
+ close_btn.on( 'click', function ( e ) {
+ e.preventDefault();
+ control.collapseForm();
+ control.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
+ } );
+ },
+
+ /**
+ * Update the title of the form if a title field is entered
+ */
+ _setupWidgetTitle: function () {
+ var control = this;
+ var update_title = function () {
+ var title = control.setting().title;
+ var in_widget_title = control.container.find( '.in-widget-title' );
+ if ( title ) {
+ in_widget_title.text( ': ' + title );
+ } else {
+ in_widget_title.text( '' );
+ }
+ };
+ control.setting.bind( update_title );
+ update_title();
+ },
+
+ /**
+ * Set up the widget-reorder-nav
+ */
+ _setupReorderUI: function () {
+ var control = this;
+
+ /**
+ * select the provided sidebar list item in the move widget area
+ *
+ * @param {jQuery} li
+ */
+ var select_sidebar_item = function ( li ) {
+ li.siblings( '.selected' ).removeClass( 'selected' );
+ li.addClass( 'selected' );
+ var is_self_sidebar = ( li.data( 'id' ) === control.params.sidebar_id );
+ control.container.find( '.move-widget-btn' ).prop( 'disabled', is_self_sidebar );
+ };
+
+ /**
+ * Add the widget reordering elements to the widget control
+ */
+ control.container.find( '.widget-title-action' ).after( $( self.tpl.widget_reorder_nav ) );
+ var move_widget_area = $(
+ _.template( self.tpl.move_widget_area, {
+ sidebars: _( self.registered_sidebars.toArray() ).pluck( 'attributes' )
+ } )
+ );
+ control.container.find( '.widget-top' ).after( move_widget_area );
+
+ /**
+ * Update available sidebars when their rendered state changes
+ */
+ var update_available_sidebars = function () {
+ var sidebar_items = move_widget_area.find( 'li' );
+ var self_sidebar_item = sidebar_items.filter( function(){
+ return $( this ).data( 'id' ) === control.params.sidebar_id;
+ } );
+ sidebar_items.each( function () {
+ var li = $( this );
+ var sidebar_id = li.data( 'id' );
+ var sidebar_model = self.registered_sidebars.get( sidebar_id );
+ li.toggle( sidebar_model.get( 'is_rendered' ) );
+ if ( li.hasClass( 'selected' ) && ! sidebar_model.get( 'is_rendered' ) ) {
+ select_sidebar_item( self_sidebar_item );
+ }
+ } );
+ };
+ update_available_sidebars();
+ self.registered_sidebars.on( 'change:is_rendered', update_available_sidebars );
+
+ /**
+ * Handle clicks for up/down/move on the reorder nav
+ */
+ var reorder_nav = control.container.find( '.widget-reorder-nav' );
+ reorder_nav.find( '.move-widget, .move-widget-down, .move-widget-up' ).on( 'click keypress', function ( event ) {
+ if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
+ return;
+ }
+ $( this ).focus();
+
+ if ( $( this ).is( '.move-widget' ) ) {
+ control.toggleWidgetMoveArea();
+ } else {
+ var is_move_down = $( this ).is( '.move-widget-down' );
+ var is_move_up = $( this ).is( '.move-widget-up' );
+ var i = control.getWidgetSidebarPosition();
+ if ( ( is_move_up && i === 0 ) || ( is_move_down && i === control.getSidebarWidgetsControl().setting().length - 1 ) ) {
+ return;
+ }
+
+ if ( is_move_up ) {
+ control.moveUp();
+ } else {
+ control.moveDown();
+ }
+
+ $( this ).focus(); // re-focus after the container was moved
+ }
+ } );
+
+ /**
+ * Handle selecting a sidebar to move to
+ */
+ control.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function ( e ) {
+ if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
+ return;
+ }
+ e.preventDefault();
+ select_sidebar_item( $( this ) );
+ } );
+
+ /**
+ * Move widget to another sidebar
+ */
+ control.container.find( '.move-widget-btn' ).click( function () {
+ control.getSidebarWidgetsControl().toggleReordering( false );
+
+ var old_sidebar_id = control.params.sidebar_id;
+ var new_sidebar_id = control.container.find( '.widget-area-select li.selected' ).data( 'id' );
+ var old_sidebar_widgets_setting = customize( 'sidebars_widgets[' + old_sidebar_id + ']' );
+ var new_sidebar_widgets_setting = customize( 'sidebars_widgets[' + new_sidebar_id + ']' );
+ var old_sidebar_widget_ids = Array.prototype.slice.call( old_sidebar_widgets_setting() );
+ var new_sidebar_widget_ids = Array.prototype.slice.call( new_sidebar_widgets_setting() );
+
+ var i = control.getWidgetSidebarPosition();
+ old_sidebar_widget_ids.splice( i, 1 );
+ new_sidebar_widget_ids.push( control.params.widget_id );
+
+ old_sidebar_widgets_setting( old_sidebar_widget_ids );
+ new_sidebar_widgets_setting( new_sidebar_widget_ids );
+
+ control.focus();
+ } );
+ },
+
+ /**
+ * Highlight widgets in preview when interacted with in the customizer
+ */
+ _setupHighlightEffects: function() {
+ var control = this;
+
+ // Highlight whenever hovering or clicking over the form
+ control.container.on( 'mouseenter click', function () {
+ control.highlightPreviewWidget();
+ } );
+
+ // Highlight when the setting is updated
+ control.setting.bind( function () {
+ control.scrollPreviewWidgetIntoView();
+ control.highlightPreviewWidget();
+ } );
+
+ // Highlight when the widget form is expanded
+ control.container.on( 'expand', function () {
+ control.scrollPreviewWidgetIntoView();
+ } );
+ },
+
+ /**
+ * Set up event handlers for widget updating
+ */
+ _setupUpdateUI: function () {
+ var control = this;
+
+ var widget_content = control.container.find( '.widget-content' );
+
+ // Configure update button
+ var save_btn = control.container.find( '.widget-control-save' );
+ save_btn.val( self.i18n.save_btn_label );
+ save_btn.attr( 'title', self.i18n.save_btn_tooltip );
+ save_btn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
+ save_btn.on( 'click', function ( e ) {
+ e.preventDefault();
+ control.updateWidget();
+ } );
+
+ var trigger_save = _.debounce( function () {
+ // @todo For compatibility with other plugins, should we trigger a click event? What about form submit event?
+ control.updateWidget();
+ }, 250 );
+
+ // Trigger widget form update when hitting Enter within an input
+ control.container.find( '.widget-content' ).on( 'keydown', 'input', function( e ) {
+ if ( 13 === e.which ) { // Enter
+ e.preventDefault();
+ control.updateWidget( { ignore_active_element: true } );
+ }
+ } );
+
+ // Handle widgets that support live previews
+ widget_content.on( 'change input propertychange', ':input', function ( e ) {
+ if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
+ trigger_save();
+ }
+ } );
+
+ // Remove loading indicators when the setting is saved and the preview updates
+ control.setting.previewer.channel.bind( 'synced', function () {
+ control.container.removeClass( 'previewer-loading' );
+ } );
+ self.previewer.bind( 'widget-updated', function ( updated_widget_id ) {
+ if ( updated_widget_id === control.params.widget_id ) {
+ control.container.removeClass( 'previewer-loading' );
+ }
+ } );
+
+ // Update widget control to indicate whether it is currently rendered (cf. Widget Visibility)
+ self.previewer.bind( 'rendered-widgets', function ( rendered_widgets ) {
+ var is_rendered = !! rendered_widgets[control.params.widget_id];
+ control.container.toggleClass( 'widget-rendered', is_rendered );
+ } );
+ },
+
+ /**
+ * Set up event handlers for widget removal
+ */
+ _setupRemoveUI: function () {
+ var control = this;
+
+ // Configure remove button
+ var remove_btn = control.container.find( 'a.widget-control-remove' );
+ // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
+ remove_btn.on( 'click', function ( e ) {
+ e.preventDefault();
+
+ // Find an adjacent element to add focus to when this widget goes away
+ var adjacent_focus_target;
+ if ( control.container.next().is( '.customize-control-widget_form' ) ) {
+ adjacent_focus_target = control.container.next().find( '.widget-action:first' );
+ } else if ( control.container.prev().is( '.customize-control-widget_form' ) ) {
+ adjacent_focus_target = control.container.prev().find( '.widget-action:first' );
+ } else {
+ adjacent_focus_target = control.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
+ }
+
+ control.container.slideUp( function() {
+ var sidebars_widgets_control = self.getSidebarWidgetControlContainingWidget( control.params.widget_id );
+ if ( ! sidebars_widgets_control ) {
+ throw new Error( 'Unable to find sidebars_widgets_control' );
+ }
+ var sidebar_widget_ids = sidebars_widgets_control.setting().slice();
+ var i = sidebar_widget_ids.indexOf( control.params.widget_id );
+ if ( -1 === i ) {
+ throw new Error( 'Widget is not in sidebar' );
+ }
+ sidebar_widget_ids.splice( i, 1 );
+ sidebars_widgets_control.setting( sidebar_widget_ids );
+ adjacent_focus_target.focus(); // keyboard accessibility
+ } );
+ } );
+
+ var replace_delete_with_remove = function () {
+ remove_btn.text( self.i18n.remove_btn_label ); // wp_widget_control() outputs the link as "Delete"
+ remove_btn.attr( 'title', self.i18n.remove_btn_tooltip );
+ };
+ if ( control.params.is_new ) {
+ wp.customize.bind( 'saved', replace_delete_with_remove );
+ } else {
+ replace_delete_with_remove();
+ }
+ },
+
+ /**
+ * Iterate over supplied inputs and create a signature string for all of them together.
+ * This string can be used to compare whether or not the form has all of the same fields.
+ *
+ * @param {jQuery} inputs
+ * @returns {string}
+ * @private
+ */
+ _getInputsSignature: function ( inputs ) {
+ var inputs_signatures = _( inputs ).map( function ( input ) {
+ input = $( input );
+ var signature_parts;
+ if ( input.is( 'option' ) ) {
+ signature_parts = [ input.prop( 'nodeName' ), input.prop( 'value' ) ];
+ } else if ( input.is( ':checkbox, :radio' ) ) {
+ signature_parts = [ input.prop( 'type' ), input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
+ } else {
+ signature_parts = [ input.prop( 'nodeName' ), input.attr( 'id' ), input.attr( 'name' ), input.attr( 'type' ) ];
+ }
+ return signature_parts.join( ',' );
+ } );
+ return inputs_signatures.join( ';' );
+ },
+
+ /**
+ * Get the property that represents the state of an input.
+ *
+ * @param {jQuery|DOMElement} input
+ * @returns {string}
+ * @private
+ */
+ _getInputStatePropertyName: function ( input ) {
+ input = $( input );
+ if ( input.is( ':radio, :checkbox' ) ) {
+ return 'checked';
+ } else if ( input.is( 'option' ) ) {
+ return 'selected';
+ } else {
+ return 'value';
+ }
+ },
+
+ /***********************************************************************
+ * Begin public API methods
+ **********************************************************************/
+
+ /**
+ * @return {wp.customize.controlConstructor.sidebar_widgets[]}
+ */
+ getSidebarWidgetsControl: function () {
+ var control = this;
+ var setting_id = 'sidebars_widgets[' + control.params.sidebar_id + ']';
+ var sidebar_widgets_control = customize.control( setting_id );
+ if ( ! sidebar_widgets_control ) {
+ throw new Error( 'Unable to locate sidebar_widgets control for ' + control.params.sidebar_id );
+ }
+ return sidebar_widgets_control;
+ },
+
+ /**
+ * Submit the widget form via Ajax and get back the updated instance,
+ * along with the new widget control form to render.
+ *
+ * @param {object} [args]
+ * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used
+ * @param {Function|null} [args.complete=null] Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
+ * @param {Boolean} [args.ignore_active_element=false] Whether or not updating a field will be deferred if focus is still on the element.
+ */
+ updateWidget: function ( args ) {
+ var control = this;
+ args = $.extend( {
+ instance: null,
+ complete: null,
+ ignore_active_element: false
+ }, args );
+ var instance_override = args.instance;
+ var complete_callback = args.complete;
+
+ control._update_count += 1;
+ var update_number = control._update_count;
+
+ var widget_content = control.container.find( '.widget-content' );
+
+ var element_id_to_refocus = null;
+ var active_input_selection_start = null;
+ var active_input_selection_end = null;
+ // @todo Support more selectors than IDs?
+ if ( $.contains( control.container[0], document.activeElement ) && $( document.activeElement ).is( '[id]' ) ) {
+ element_id_to_refocus = $( document.activeElement ).prop( 'id' );
+ // @todo IE8 support: http://stackoverflow.com/a/4207763/93579
+ try {
+ active_input_selection_start = document.activeElement.selectionStart;
+ active_input_selection_end = document.activeElement.selectionEnd;
+ }
+ catch( e ) {} // catch InvalidStateError in case of checkboxes
+ }
+
+ control.container.addClass( 'widget-form-loading' );
+ control.container.addClass( 'previewer-loading' );
+
+ var params = {};
+ params.action = self.update_widget_ajax_action;
+ params[self.update_widget_nonce_post_key] = self.update_widget_nonce_value;
+
+ var data = $.param( params );
+ var inputs = widget_content.find( ':input, option' );
+
+ // Store the value we're submitting in data so that when the response comes back,
+ // we know if it got sanitized; if there is no difference in the sanitized value,
+ // then we do not need to touch the UI and mess up the user's ongoing editing.
+ inputs.each( function () {
+ var input = $( this );
+ var property = control._getInputStatePropertyName( this );
+ input.data( 'state' + update_number, input.prop( property ) );
+ } );
+
+ if ( instance_override ) {
+ data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instance_override ) } );
+ } else {
+ data += '&' + inputs.serialize();
+ }
+ data += '&' + widget_content.find( '~ :input' ).serialize();
+
+ console.log( wp.ajax.settings.url, data );
+ var jqxhr = $.post( wp.ajax.settings.url, data, function ( r ) {
+ if ( r.success ) {
+ var sanitized_form = $( '<div>' + r.data.form + '</div>' );
+ control.hook( 'formUpdate', sanitized_form );
+
+ var sanitized_inputs = sanitized_form.find( ':input, option' );
+ var has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
+
+ if ( has_same_inputs_in_response ) {
+ inputs.each( function ( i ) {
+ var input = $( this );
+ var sanitized_input = $( sanitized_inputs[i] );
+ var property = control._getInputStatePropertyName( this );
+ var state = input.data( 'state' + update_number );
+ var sanitized_state = sanitized_input.prop( property );
+ input.data( 'sanitized', sanitized_state );
+
+ if ( state !== sanitized_state ) {
+
+ // Only update now if not currently focused on it,
+ // so that we don't cause the cursor
+ // it will be updated upon the change event
+ if ( args.ignore_active_element || ! input.is( document.activeElement ) ) {
+ input.prop( property, sanitized_state );
+ }
+ control.hook( 'unsanitaryField', input, sanitized_state, state );
+
+ } else {
+ control.hook( 'sanitaryField', input, state );
+ }
+ } );
+ control.hook( 'formUpdated', sanitized_form );
+ } else {
+ widget_content.html( sanitized_form.html() );
+ if ( element_id_to_refocus ) {
+ // not using jQuery selector so we don't have to worry about escaping IDs with brackets and other characters
+ $( document.getElementById( element_id_to_refocus ) )
+ .prop( {
+ selectionStart: active_input_selection_start,
+ selectionEnd: active_input_selection_end
+ } )
+ .focus();
+ }
+ control.hook( 'formRefreshed' );
+ }
+
+ /**
+ * If the old instance is identical to the new one, there is nothing new
+ * needing to be rendered, and so we can preempt the event for the
+ * preview finishing loading.
+ */
+ var is_instance_identical = _( control.setting() ).isEqual( r.data.instance );
+ if ( is_instance_identical ) {
+ control.container.removeClass( 'previewer-loading' );
+ } else {
+ control.is_widget_updating = true; // suppress triggering another updateWidget
+ control.setting( r.data.instance );
+ control.is_widget_updating = false;
+ }
+
+ if ( complete_callback ) {
+ complete_callback.call( control, null, { no_change: is_instance_identical, ajax_finished: true } );
+ }
+ } else {
+ console.log( r );
+ var message = 'FAIL';
+ if ( r.data && r.data.message ) {
+ message = r.data.message;
+ }
+ if ( complete_callback ) {
+ complete_callback.call( control, message );
+ } else {
+ throw new Error( message );
+ }
+ }
+ } );
+ jqxhr.fail( function ( jqXHR, textStatus ) {
+ if ( complete_callback ) {
+ complete_callback.call( control, textStatus );
+ } else {
+ throw new Error( textStatus );
+ }
+ } );
+ jqxhr.always( function () {
+ control.container.removeClass( 'widget-form-loading' );
+ inputs.each( function () {
+ $( this ).removeData( 'state' + update_number );
+ } );
+ } );
+ },
+
+ /**
+ * Expand the accordion section containing a control
+ * @todo it would be nice if accordion had a proper API instead of having to trigger UI events on its elements
+ */
+ expandControlSection: function () {
+ var section = this.container.closest( '.accordion-section' );
+ if ( ! section.hasClass( 'open' ) ) {
+ section.find( '.accordion-section-title:first' ).trigger( 'click' );
+ }
+ },
+
+ /**
+ * Expand the widget form control
+ */
+ expandForm: function () {
+ this.toggleForm( true );
+ },
+
+ /**
+ * Collapse the widget form control
+ */
+ collapseForm: function () {
+ this.toggleForm( false );
+ },
+
+ /**
+ * Expand or collapse the widget control
+ *
+ * @param {boolean|undefined} [do_expand] If not supplied, will be inverse of current visibility
+ */
+ toggleForm: function ( do_expand ) {
+ var control = this;
+ var widget = control.container.find( 'div.widget:first' );
+ var inside = widget.find( '.widget-inside:first' );
+ if ( typeof do_expand === 'undefined' ) {
+ do_expand = ! inside.is( ':visible' );
+ }
+
+ // Already expanded or collapsed, so noop
+ if ( inside.is( ':visible' ) === do_expand ) {
+ return;
+ }
+
+ var complete;
+ if ( do_expand ) {
+ // Close all other widget controls before expanding this one
+ wp.customize.control.each( function ( other_control ) {
+ if ( control.params.type === other_control.params.type && control !== other_control ) {
+ other_control.collapseForm();
+ }
+ } );
+
+ control.container.trigger( 'expand' );
+ control.container.addClass( 'expanding' );
+ complete = function () {
+ control.container.removeClass( 'expanding' );
+ control.container.addClass( 'expanded' );
+ control.container.trigger( 'expanded' );
+ };
+ if ( control.params.is_wide ) {
+ inside.animate( { width: 'show' }, 'fast', complete );
+ } else {
+ inside.slideDown( 'fast', complete );
+ }
+ } else {
+ control.container.trigger( 'collapse' );
+ control.container.addClass( 'collapsing' );
+ complete = function () {
+ control.container.removeClass( 'collapsing' );
+ control.container.removeClass( 'expanded' );
+ control.container.trigger( 'collapsed' );
+ };
+ if ( control.params.is_wide ) {
+ inside.animate( { width: 'hide' }, 'fast', complete );
+ } else {
+ inside.slideUp( 'fast', function() {
+ widget.css( { width:'', margin:'' } );
+ complete();
+ } );
+ }
+ }
+ },
+
+ /**
+ * Expand the containing sidebar section, expand the form, and focus on
+ * the first input in the control
+ */
+ focus: function () {
+ var control = this;
+ control.expandControlSection();
+ control.expandForm();
+ control.container.find( ':focusable:first' ).focus().trigger( 'click' );
+ },
+
+ /**
+ * Get the position (index) of the widget in the containing sidebar
+ *
+ * @throws Error
+ * @returns {Number}
+ */
+ getWidgetSidebarPosition: function () {
+ var control = this;
+ var sidebar_widget_ids = control.getSidebarWidgetsControl().setting();
+ var position = sidebar_widget_ids.indexOf( control.params.widget_id );
+ if ( position === -1 ) {
+ throw new Error( 'Widget was unexpectedly not present in the sidebar.' );
+ }
+ return position;
+ },
+
+ /**
+ * Move widget up one in the sidebar
+ */
+ moveUp: function () {
+ this._moveWidgetByOne( -1 );
+ },
+
+ /**
+ * Move widget up one in the sidebar
+ */
+ moveDown: function () {
+ this._moveWidgetByOne( 1 );
+ },
+
+ /**
+ * @private
+ *
+ * @param {Number} offset 1|-1
+ */
+ _moveWidgetByOne: function ( offset ) {
+ var control = this;
+ var i = control.getWidgetSidebarPosition();
+
+ var sidebar_widgets_setting = control.getSidebarWidgetsControl().setting;
+ var sidebar_widget_ids = Array.prototype.slice.call( sidebar_widgets_setting() ); // clone
+ var adjacent_widget_id = sidebar_widget_ids[i + offset];
+ sidebar_widget_ids[i + offset] = control.params.widget_id;
+ sidebar_widget_ids[i] = adjacent_widget_id;
+
+ sidebar_widgets_setting( sidebar_widget_ids );
+ },
+
+ /**
+ * Toggle visibility of the widget move area
+ *
+ * @param {Boolean} [toggle]
+ */
+ toggleWidgetMoveArea: function ( toggle ) {
+ var control = this;
+ var move_widget_area = control.container.find( '.move-widget-area' );
+ if ( typeof toggle === 'undefined' ) {
+ toggle = ! move_widget_area.hasClass( 'active' );
+ }
+ if ( toggle ) {
+ // reset the selected sidebar
+ move_widget_area.find( '.selected' ).removeClass( 'selected' );
+ move_widget_area.find( 'li' ).filter( function () {
+ return $( this ).data( 'id' ) === control.params.sidebar_id;
+ } ).addClass( 'selected' );
+ control.container.find( '.move-widget-btn' ).prop( 'disabled', true );
+ }
+ move_widget_area.toggleClass( 'active', toggle );
+ },
+
+ /**
+ * Inverse of WidgetCustomizer.getControlInstanceForWidget
+ * @return {jQuery}
+ */
+ getPreviewWidgetElement: function () {
+ var control = this;
+ var widget_customizer_preview = self.getPreviewWindow().WidgetCustomizerPreview;
+ return widget_customizer_preview.getSidebarWidgetElement( control.params.sidebar_id, control.params.widget_id );
+ },
+
+ /**
+ * Inside of the customizer preview, scroll the widget into view
+ */
+ scrollPreviewWidgetIntoView: function () {
+ // @todo scrollIntoView() provides a robust but very poor experience. Animation is needed. See https://github.com/x-team/wp-widget-customizer/issues/16
+ },
+
+ /**
+ * Highlight the widget control and section
+ */
+ highlightSectionAndControl: function() {
+ var control = this;
+ var target_element;
+ if ( control.container.is( ':hidden' ) ) {
+ target_element = control.container.closest( '.control-section' );
+ } else {
+ target_element = control.container;
+ }
+
+ $( '.widget-customizer-highlighted' ).removeClass( 'widget-customizer-highlighted' );
+ target_element.addClass( 'widget-customizer-highlighted' );
+ setTimeout( function () {
+ target_element.removeClass( 'widget-customizer-highlighted' );
+ }, 500 );
+ },
+
+ /**
+ * Add the widget-customizer-highlighted-widget class to the widget for 500ms
+ */
+ highlightPreviewWidget: function () {
+ var control = this;
+ var widget_el = control.getPreviewWidgetElement();
+ var root_el = widget_el.closest( 'html' );
+ root_el.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
+ widget_el.addClass( 'widget-customizer-highlighted-widget' );
+ setTimeout( function () {
+ widget_el.removeClass( 'widget-customizer-highlighted-widget' );
+ }, 500 );
+ }
+
+ } );
+
+ /**
+ * Capture the instance of the Previewer since it is private
+ */
+ var OldPreviewer = wp.customize.Previewer;
+ wp.customize.Previewer = OldPreviewer.extend( {
+ initialize: function( params, options ) {
+ self.previewer = this;
+ OldPreviewer.prototype.initialize.call( this, params, options );
+ this.bind( 'refresh', this.refresh );
+ }
+ } );
+
+ /**
+ * Given a widget control, find the sidebar widgets control that contains it.
+ * @param {string} widget_id
+ * @return {object|null}
+ */
+ self.getSidebarWidgetControlContainingWidget = function ( widget_id ) {
+ var found_control = null;
+ // @todo this can use widget_id_to_setting_id(), then pass into wp.customize.control( x ).getSidebarWidgetsControl()
+ wp.customize.control.each( function ( control ) {
+ if ( control.params.type === 'sidebar_widgets' && -1 !== control.setting().indexOf( widget_id ) ) {
+ found_control = control;
+ }
+ } );
+ return found_control;
+ };
+
+ /**
+ * Given a widget_id for a widget appearing in the preview, get the widget form control associated with it
+ * @param {string} widget_id
+ * @return {object|null}
+ */
+ self.getWidgetFormControlForWidget = function ( widget_id ) {
+ var found_control = null;
+ // @todo We can just use widget_id_to_setting_id() here
+ wp.customize.control.each( function ( control ) {
+ if ( control.params.type === 'widget_form' && control.params.widget_id === widget_id ) {
+ found_control = control;
+ }
+ } );
+ return found_control;
+ };
+
+ /**
+ * @returns {Window}
+ */
+ self.getPreviewWindow = function (){
+ return $( '#customize-preview' ).find( 'iframe' ).prop( 'contentWindow' );
+ };
+
+ /**
+ * Available Widgets Panel
+ */
+ self.availableWidgetsPanel = {
+ active_sidebar_widgets_control: null,
+ selected_widget_tpl: null,
+ container: null,
+ filter_input: null,
+
+ /**
+ * Set up event listeners
+ */
+ setup: function () {
+ var panel = this;
+ panel.container = $( '#available-widgets' );
+ panel.filter_input = $( '#available-widgets-filter' ).find( 'input' );
+
+ var update_available_widgets_list = function () {
+ self.available_widgets.each( function ( widget ) {
+ var widget_tpl = $( '#widget-tpl-' + widget.id );
+ widget_tpl.toggle( ! widget.get( 'is_disabled' ) );
+ if ( widget.get( 'is_disabled' ) && widget_tpl.is( panel.selected_widget_tpl ) ) {
+ panel.selected_widget_tpl = null;
+ }
+ } );
+ };
+
+ self.available_widgets.on( 'change', update_available_widgets_list );
+ update_available_widgets_list();
+
+ // If the available widgets panel is open and the customize controls are
+ // interacted with (i.e. available widgets panel is blurred) then close the
+ // available widgets panel.
+ $( '#customize-controls' ).on( 'click keydown', function ( e ) {
+ var is_add_new_widget_btn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
+ if ( $( 'body' ).hasClass( 'adding-widget' ) && ! is_add_new_widget_btn ) {
+ panel.close();
+ }
+ } );
+
+ // Close the panel if the URL in the preview changes
+ self.previewer.bind( 'url', function () {
+ panel.close();
+ } );
+
+ // Submit a selection when clicked or keypressed
+ panel.container.find( '.widget-tpl' ).on( 'click keypress', function( event ) {
+
+ // Only proceed with keypress if it is Enter or Spacebar
+ if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
+ return;
+ }
+
+ panel.submit( this );
+ } );
+
+ panel.container.liveFilter(
+ '#available-widgets-filter input',
+ '.widget-tpl',
+ {
+ filterChildSelector: '.widget-title h4',
+ after: function () {
+ var filter_val = panel.filter_input.val();
+
+ // Remove a widget from being selected if it is no longer visible
+ if ( panel.selected_widget_tpl && ! panel.selected_widget_tpl.is( ':visible' ) ) {
+ panel.selected_widget_tpl.removeClass( 'selected' );
+ panel.selected_widget_tpl = null;
+ }
+
+ // If a widget was selected but the filter value has been cleared out, clear selection
+ if ( panel.selected_widget_tpl && ! filter_val ) {
+ panel.selected_widget_tpl.removeClass( 'selected' );
+ panel.selected_widget_tpl = null;
+ }
+
+ // If a filter has been entered and a widget hasn't been selected, select the first one shown
+ if ( ! panel.selected_widget_tpl && filter_val ) {
+ var first_visible_widget = panel.container.find( '> .widget-tpl:visible:first' );
+ if ( first_visible_widget.length ) {
+ panel.select( first_visible_widget );
+ }
+ }
+
+ }
+ }
+ );
+
+ // Select a widget when it is focused on
+ panel.container.find( ' > .widget-tpl' ).on( 'focus', function () {
+ panel.select( this );
+ } );
+
+ panel.container.on( 'keydown', function ( event ) {
+ var is_enter = ( event.which === 13 );
+ var is_esc = ( event.which === 27 );
+ var is_down = ( event.which === 40 );
+ var is_up = ( event.which === 38 );
+ var selected_widget_tpl = null;
+ var first_visible_widget = panel.container.find( '> .widget-tpl:visible:first' );
+ var last_visible_widget = panel.container.find( '> .widget-tpl:visible:last' );
+ var is_input_focused = $( event.target ).is( panel.filter_input );
+
+ if ( is_down || is_up ) {
+ if ( is_down ) {
+ if ( is_input_focused ) {
+ selected_widget_tpl = first_visible_widget;
+ } else if ( panel.selected_widget_tpl && panel.selected_widget_tpl.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
+ selected_widget_tpl = panel.selected_widget_tpl.nextAll( '.widget-tpl:visible:first' );
+ }
+ } else if ( is_up ) {
+ if ( is_input_focused ) {
+ selected_widget_tpl = last_visible_widget;
+ } else if ( panel.selected_widget_tpl && panel.selected_widget_tpl.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
+ selected_widget_tpl = panel.selected_widget_tpl.prevAll( '.widget-tpl:visible:first' );
+ }
+ }
+ panel.select( selected_widget_tpl );
+ if ( selected_widget_tpl ) {
+ selected_widget_tpl.focus();
+ } else {
+ panel.filter_input.focus();
+ }
+ return;
+ }
+
+ // If enter pressed but nothing entered, don't do anything
+ if ( is_enter && ! panel.filter_input.val() ) {
+ return;
+ }
+
+ if ( is_enter ) {
+ panel.submit();
+ } else if ( is_esc ) {
+ panel.close( { return_focus: true } );
+ }
+ } );
+ },
+
+ /**
+ * @param widget_tpl
+ */
+ select: function ( widget_tpl ) {
+ var panel = this;
+ panel.selected_widget_tpl = $( widget_tpl );
+ panel.selected_widget_tpl.siblings( '.widget-tpl' ).removeClass( 'selected' );
+ panel.selected_widget_tpl.addClass( 'selected' );
+ },
+
+ submit: function ( widget_tpl ) {
+ var panel = this;
+ if ( ! widget_tpl ) {
+ widget_tpl = panel.selected_widget_tpl;
+ }
+ if ( ! widget_tpl || ! panel.active_sidebar_widgets_control ) {
+ return;
+ }
+ panel.select( widget_tpl );
+
+ var widget_id = $( panel.selected_widget_tpl ).data( 'widget-id' );
+ var widget = self.available_widgets.findWhere( {id: widget_id} );
+ if ( ! widget ) {
+ throw new Error( 'Widget unexpectedly not found.' );
+ }
+ panel.active_sidebar_widgets_control.addWidget( widget.get( 'id_base' ) );
+ panel.close();
+ },
+
+ /**
+ * @param sidebars_widgets_control
+ */
+ open: function ( sidebars_widgets_control ) {
+ var panel = this;
+ panel.active_sidebar_widgets_control = sidebars_widgets_control;
+
+ // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens
+ _( sidebars_widgets_control.getWidgetFormControls() ).each( function ( control ) {
+ if ( control.params.is_wide ) {
+ control.collapseForm();
+ }
+ } );
+
+ $( 'body' ).addClass( 'adding-widget' );
+ panel.container.find( '.widget-tpl' ).removeClass( 'selected' );
+ panel.filter_input.focus();
+ },
+
+ /**
+ * Hide the panel
+ */
+ close: function ( options ) {
+ var panel = this;
+ options = options || {};
+ if ( options.return_focus && panel.active_sidebar_widgets_control ) {
+ panel.active_sidebar_widgets_control.container.find( '.add-new-widget' ).focus();
+ }
+ panel.active_sidebar_widgets_control = null;
+ panel.selected_widget_tpl = null;
+ $( 'body' ).removeClass( 'adding-widget' );
+ panel.filter_input.val( '' );
+ }
+ };
+
+ /**
+ * @param {String} widget_id
+ * @returns {Object}
+ */
+ function parse_widget_id( widget_id ) {
+ var parsed = {
+ number: null,
+ id_base: null
+ };
+ var matches = widget_id.match( /^(.+)-(\d+)$/ );
+ if ( matches ) {
+ parsed.id_base = matches[1];
+ parsed.number = parseInt( matches[2], 10 );
+ } else {
+ // likely an old single widget
+ parsed.id_base = widget_id;
+ }
+ return parsed;
+ }
+
+ /**
+ * @param {String} widget_id
+ * @returns {String} setting_id
+ */
+ function widget_id_to_setting_id( widget_id ) {
+ var parsed = parse_widget_id( widget_id );
+ var setting_id = 'widget_' + parsed.id_base;
+ if ( parsed.number ) {
+ setting_id += '[' + parsed.number + ']';
+ }
+ return setting_id;
+ }
+
+ return self;
+}( jQuery ));
+
+/* @todo remove this dependency */
+/*
+ * jQuery.liveFilter
+ *
+ * Copyright (c) 2009 Mike Merritt
+ *
+ * Forked by Lim Chee Aun (cheeaun.com)
+ *
+ */
+
+(function($){
+ $.fn.liveFilter = function(inputEl, filterEl, options){
+ var defaults = {
+ filterChildSelector: null,
+ filter: function(el, val){
+ return $(el).text().toUpperCase().indexOf(val.toUpperCase()) >= 0;
+ },
+ before: function(){},
+ after: function(){}
+ };
+ options = $.extend(defaults, options);
+
+ var el = $(this).find(filterEl);
+ if (options.filterChildSelector) {
+ el = el.find(options.filterChildSelector);
+ }
+
+ var filter = options.filter;
+ $(inputEl).keyup(function(){
+ var val = $(this).val();
+ var contains = el.filter(function(){
+ return filter(this, val);
+ });
+ var containsNot = el.not(contains);
+ if (options.filterChildSelector){
+ contains = contains.parents(filterEl);
+ containsNot = containsNot.parents(filterEl).hide();
+ }
+
+ options.before.call(this, contains, containsNot);
+
+ contains.show();
+ containsNot.hide();
+
+ if (val === '') {
+ contains.show();
+ containsNot.show();
+ }
+
+ options.after.call(this, contains, containsNot);
+ });
+ };
+})(jQuery);
</ins></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizecontrolphp"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/class-wp-customize-control.php (27418 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/class-wp-customize-control.php 2014-03-05 20:03:33 UTC (rev 27418)
+++ trunk/src/wp-includes/class-wp-customize-control.php 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -813,4 +813,76 @@
</span><span class="cx"> foreach ( $this->default_headers as $choice => $header )
</span><span class="cx"> $this->print_header_image( $choice, $header );
</span><span class="cx"> }
</span><del>-}
</del><span class="cx">\ No newline at end of file
</span><ins>+}
+
+/**
+ * Widget Area Customize Control Class
+ *
+ */
+class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
+ public $type = 'sidebar_widgets';
+ public $sidebar_id;
+
+ public function to_json() {
+ parent::to_json();
+ $exported_properties = array( 'sidebar_id' );
+ foreach ( $exported_properties as $key ) {
+ $this->json[ $key ] = $this->$key;
+ }
+ }
+
+ public function render_content() {
+ ?>
+ <span class="button-secondary add-new-widget" tabindex="0">
+ <?php esc_html_e( 'Add a Widget' ); ?>
+ </span>
+
+ <span class="reorder-toggle" tabindex="0">
+ <span class="reorder"><?php esc_html_e( 'Reorder' ); ?></span>
+ <span class="reorder-done"><?php esc_html_e( 'Done' ); ?></span>
+ </span>
+ <?php
+ }
+}
+
+/**
+ * Widget Form Customize Control Class
+ */
+class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
+ public $type = 'widget_form';
+ public $widget_id;
+ public $widget_id_base;
+ public $sidebar_id;
+ public $is_new = false;
+ public $width;
+ public $height;
+ public $is_wide = false;
+ public $is_live_previewable = false;
+
+ public function to_json() {
+ parent::to_json();
+ $exported_properties = array( 'widget_id', 'widget_id_base', 'sidebar_id', 'width', 'height', 'is_wide', 'is_live_previewable' );
+ foreach ( $exported_properties as $key ) {
+ $this->json[ $key ] = $this->$key;
+ }
+ }
+
+ public function render_content() {
+ global $wp_registered_widgets;
+ require_once ABSPATH . '/wp-admin/includes/widgets.php';
+
+ $widget = $wp_registered_widgets[ $this->widget_id ];
+ if ( ! isset( $widget['params'][0] ) ) {
+ $widget['params'][0] = array();
+ }
+
+ $args = array(
+ 'widget_id' => $widget['id'],
+ 'widget_name' => $widget['name'],
+ );
+
+ $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
+ echo WP_Customize_Widgets::get_widget_control( $args );
+ }
+}
+
</ins></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/class-wp-customize-manager.php (27418 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/class-wp-customize-manager.php 2014-03-05 20:03:33 UTC (rev 27418)
+++ trunk/src/wp-includes/class-wp-customize-manager.php 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -61,7 +61,10 @@
</span><span class="cx"> require( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
</span><span class="cx"> require( ABSPATH . WPINC . '/class-wp-customize-section.php' );
</span><span class="cx"> require( ABSPATH . WPINC . '/class-wp-customize-control.php' );
</span><ins>+ require( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
</ins><span class="cx">
</span><ins>+ WP_Customize_Widgets::setup(); // This should be integrated.
+
</ins><span class="cx"> add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
</span><span class="cx">
</span><span class="cx"> add_action( 'setup_theme', array( $this, 'setup_theme' ) );
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizewidgetsphp"></a>
<div class="addfile"><h4>Added: trunk/src/wp-includes/class-wp-customize-widgets.php (0 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/class-wp-customize-widgets.php (rev 0)
+++ trunk/src/wp-includes/class-wp-customize-widgets.php 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -0,0 +1,1264 @@
</span><ins>+<?php
+/**
+ * Widget customizer manager class.
+ */
+class WP_Customize_Widgets {
+ const UPDATE_WIDGET_AJAX_ACTION = 'update-widget';
+ const UPDATE_WIDGET_NONCE_POST_KEY = 'update-sidebar-widgets-nonce';
+
+ /**
+ * All id_bases for widgets defined in core
+ *
+ * @var array
+ */
+ protected static $core_widget_id_bases = array(
+ 'archives',
+ 'calendar',
+ 'categories',
+ 'links',
+ 'meta',
+ 'nav_menu',
+ 'pages',
+ 'recent-comments',
+ 'recent-posts',
+ 'rss',
+ 'search',
+ 'tag_cloud',
+ 'text',
+ );
+
+ /**
+ * Initial loader.
+ */
+ static function setup() {
+ add_action( 'after_setup_theme', array( __CLASS__, 'setup_widget_addition_previews' ) );
+ add_action( 'customize_controls_init', array( __CLASS__, 'customize_controls_init' ) );
+ add_action( 'customize_register', array( __CLASS__, 'schedule_customize_register' ), 1 );
+ add_action( 'customize_controls_enqueue_scripts', array( __CLASS__, 'customize_controls_enqueue_deps' ) );
+ add_action( 'customize_controls_print_footer_scripts', array( __CLASS__, 'output_widget_control_templates' ) );
+ add_action( 'customize_preview_init', array( __CLASS__, 'customize_preview_init' ) );
+
+ add_action( 'dynamic_sidebar', array( __CLASS__, 'tally_rendered_widgets' ) );
+ add_action( 'dynamic_sidebar', array( __CLASS__, 'tally_sidebars_via_dynamic_sidebar_actions' ) );
+ add_filter( 'temp_is_active_sidebar', array( __CLASS__, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
+ add_filter( 'temp_dynamic_sidebar_has_widgets', array( __CLASS__, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
+
+ /**
+ * Special filter for Settings Revisions plugin until it can handle
+ * dynamically creating settings. Normally this should be handled by
+ * a setting's sanitize_js_callback, but when restoring an old revision
+ * it may include settings which do not currently exist, and so they
+ * do not have the opportunity to be sanitized as needed. Furthermore,
+ * we have to add this filter here because the customizer is not
+ * initialized in WP Ajax, which is where Settings Revisions currently
+ * needs to apply this filter at times.
+ */
+ add_filter( 'temp_customize_sanitize_js', array( __CLASS__, 'temp_customize_sanitize_js' ), 10, 2 );
+ }
+
+ /**
+ * Get an unslashed post value, or return a default
+ *
+ * @param string $name
+ * @param mixed $default
+ * @return mixed
+ */
+ static function get_post_value( $name, $default = null ) {
+ if ( ! isset( $_POST[$name] ) ) {
+ return $default;
+ }
+ return wp_unslash( $_POST[$name] );
+ }
+
+ protected static $_customized;
+ protected static $_prepreview_added_filters = array();
+
+ /**
+ * Since the widgets get registered (widgets_init) before the customizer settings are set up (customize_register),
+ * we have to filter the options similarly to how the setting previewer will filter the options later.
+ *
+ * @action after_setup_theme
+ */
+ static function setup_widget_addition_previews() {
+ global $wp_customize;
+ $is_customize_preview = (
+ ( ! empty( $wp_customize ) )
+ &&
+ ( ! is_admin() )
+ &&
+ ( 'on' === self::get_post_value( 'wp_customize' ) )
+ &&
+ check_ajax_referer( 'preview-customize_' . $wp_customize->get_stylesheet(), 'nonce', false )
+ );
+
+ $is_ajax_widget_update = (
+ ( defined( 'DOING_AJAX' ) && DOING_AJAX )
+ &&
+ self::get_post_value( 'action' ) === self::UPDATE_WIDGET_AJAX_ACTION
+ &&
+ check_ajax_referer( self::UPDATE_WIDGET_AJAX_ACTION, self::UPDATE_WIDGET_NONCE_POST_KEY, false )
+ );
+
+ $is_ajax_customize_save = (
+ ( defined( 'DOING_AJAX' ) && DOING_AJAX )
+ &&
+ self::get_post_value( 'action' ) === 'customize_save'
+ &&
+ check_ajax_referer( 'save-customize_' . $wp_customize->get_stylesheet(), 'nonce' )
+ );
+
+ $is_valid_request = ( $is_ajax_widget_update || $is_customize_preview || $is_ajax_customize_save );
+ if ( ! $is_valid_request ) {
+ return;
+ }
+
+ // Input from customizer preview
+ if ( isset( $_POST['customized'] ) ) {
+ $customized = json_decode( self::get_post_value( 'customized' ), true );
+ }
+ // Input from ajax widget update request
+ else {
+ $customized = array();
+ $id_base = self::get_post_value( 'id_base' );
+ $widget_number = (int) self::get_post_value( 'widget_number' );
+ $option_name = 'widget_' . $id_base;
+ $customized[$option_name] = array();
+ if ( false !== $widget_number ) {
+ $option_name .= '[' . $widget_number . ']';
+ $customized[$option_name][$widget_number] = array();
+ }
+ }
+
+ $function = array( __CLASS__, 'prepreview_added_sidebars_widgets' );
+
+ $hook = 'option_sidebars_widgets';
+ add_filter( $hook, $function );
+ self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+ $hook = 'default_option_sidebars_widgets';
+ add_filter( $hook, $function );
+ self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+ foreach ( $customized as $setting_id => $value ) {
+ if ( preg_match( '/^(widget_.+?)(\[(\d+)\])?$/', $setting_id, $matches ) ) {
+ $body = sprintf( 'return %s::prepreview_added_widget_instance( $value, %s );', __CLASS__, var_export( $setting_id, true ) );
+ $function = create_function( '$value', $body );
+ $option = $matches[1];
+
+ $hook = sprintf( 'option_%s', $option );
+ add_filter( $hook, $function );
+ self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+ $hook = sprintf( 'default_option_%s', $option );
+ add_filter( $hook, $function );
+ self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+ /**
+ * Make sure the option is registered so that the update_option won't fail due to
+ * the filters providing a default value, which causes the update_option() to get confused.
+ */
+ add_option( $option, array() );
+ }
+ }
+
+ self::$_customized = $customized;
+ }
+
+ /**
+ * Ensure that newly-added widgets will appear in the widgets_sidebars.
+ * This is necessary because the customizer's setting preview filters are added after the widgets_init action,
+ * which is too late for the widgets to be set up properly.
+ *
+ * @param array $sidebars_widgets
+ * @return array
+ */
+ static function prepreview_added_sidebars_widgets( $sidebars_widgets ) {
+ foreach ( self::$_customized as $setting_id => $value ) {
+ if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) {
+ $sidebar_id = $matches[1];
+ $sidebars_widgets[$sidebar_id] = $value;
+ }
+ }
+ return $sidebars_widgets;
+ }
+
+ /**
+ * Ensure that newly-added widgets will have empty instances so that they will be recognized.
+ * This is necessary because the customizer's setting preview filters are added after the widgets_init action,
+ * which is too late for the widgets to be set up properly.
+ *
+ * @param array $instance
+ * @param string $setting_id
+ * @return array
+ */
+ static function prepreview_added_widget_instance( $instance, $setting_id ) {
+ if ( isset( self::$_customized[$setting_id] ) ) {
+ $parsed_setting_id = self::parse_widget_setting_id( $setting_id );
+ $widget_number = $parsed_setting_id['number'];
+
+ // Single widget
+ if ( is_null( $widget_number ) ) {
+ if ( false === $instance && empty( $value ) ) {
+ $instance = array();
+ }
+ }
+ // Multi widget
+ else if ( false === $instance || ! isset( $instance[$widget_number] ) ) {
+ if ( empty( $instance ) ) {
+ $instance = array( '_multiwidget' => 1 );
+ }
+ if ( ! isset( $instance[$widget_number] ) ) {
+ $instance[$widget_number] = array();
+ }
+ }
+ }
+ return $instance;
+ }
+
+ /**
+ * Remove filters added in setup_widget_addition_previews() which ensure that
+ * widgets are populating the options during widgets_init
+ *
+ * @action wp_loaded
+ */
+ static function remove_prepreview_filters() {
+ foreach ( self::$_prepreview_added_filters as $prepreview_added_filter ) {
+ remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] );
+ }
+ self::$_prepreview_added_filters = array();
+ }
+
+ /**
+ * Make sure that all widgets get loaded into customizer; these actions are also done in the wp_ajax_save_widget()
+ *
+ * @see wp_ajax_save_widget()
+ * @action customize_controls_init
+ */
+ static function customize_controls_init() {
+ do_action( 'load-widgets.php' );
+ do_action( 'widgets.php' );
+ do_action( 'sidebar_admin_setup' );
+ }
+
+ /**
+ * When in preview, invoke customize_register for settings after WordPress is
+ * loaded so that all filters have been initialized (e.g. Widget Visibility)
+ */
+ static function schedule_customize_register( $wp_customize ) {
+ if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
+ self::customize_register( $wp_customize );
+ } else {
+ add_action( 'wp', array( __CLASS__, 'customize_register' ) );
+ }
+ }
+
+ /**
+ * Register customizer settings and controls for all sidebars and widgets
+ *
+ * @action customize_register
+ */
+ static function customize_register( $wp_customize = null ) {
+ global $wp_registered_widgets, $wp_registered_widget_controls;
+ if ( ! ( $wp_customize instanceof WP_Customize_Manager ) ) {
+ $wp_customize = $GLOBALS['wp_customize'];
+ }
+
+ $sidebars_widgets = array_merge(
+ array( 'wp_inactive_widgets' => array() ),
+ array_fill_keys( array_keys( $GLOBALS['wp_registered_sidebars'] ), array() ),
+ wp_get_sidebars_widgets()
+ );
+
+ $new_setting_ids = array();
+
+ /**
+ * Register a setting for all widgets, including those which are active, inactive, and orphaned
+ * since a widget may get suppressed from a sidebar via a plugin (like Widget Visibility).
+ */
+ foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
+ $setting_id = self::get_setting_id( $widget_id );
+ $setting_args = self::get_setting_args( $setting_id );
+ $setting_args['sanitize_callback'] = array( __CLASS__, 'sanitize_widget_instance' );
+ $setting_args['sanitize_js_callback'] = array( __CLASS__, 'sanitize_widget_js_instance' );
+ $wp_customize->add_setting( $setting_id, $setting_args );
+ $new_setting_ids[] = $setting_id;
+ }
+
+ foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
+ if ( empty( $sidebar_widget_ids ) ) {
+ $sidebar_widget_ids = array();
+ }
+ $is_registered_sidebar = isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] );
+ $is_inactive_widgets = ( 'wp_inactive_widgets' === $sidebar_id );
+ $is_active_sidebar = ( $is_registered_sidebar && ! $is_inactive_widgets );
+
+ /**
+ * Add setting for managing the sidebar's widgets
+ */
+ if ( $is_registered_sidebar || $is_inactive_widgets ) {
+ $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
+ $setting_args = self::get_setting_args( $setting_id );
+ $setting_args['sanitize_callback'] = array( __CLASS__, 'sanitize_sidebar_widgets' );
+ $setting_args['sanitize_js_callback'] = array( __CLASS__, 'sanitize_sidebar_widgets_js_instance' );
+ $wp_customize->add_setting( $setting_id, $setting_args );
+ $new_setting_ids[] = $setting_id;
+
+ /**
+ * Add section to contain controls
+ */
+ $section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
+ if ( $is_active_sidebar ) {
+ $section_args = array(
+ 'title' => sprintf( __( 'Widgets: %s' ), $GLOBALS['wp_registered_sidebars'][$sidebar_id]['name'] ),
+ 'description' => $GLOBALS['wp_registered_sidebars'][$sidebar_id]['description'],
+ );
+ $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
+ $wp_customize->add_section( $section_id, $section_args );
+
+ $control = new WP_Widget_Area_Customize_Control(
+ $wp_customize,
+ $setting_id,
+ array(
+ 'section' => $section_id,
+ 'sidebar_id' => $sidebar_id,
+ //'priority' => 99, // so it appears at the end
+ )
+ );
+ $new_setting_ids[] = $setting_id;
+ $wp_customize->add_control( $control );
+ }
+ }
+
+ /**
+ * Add a control for each active widget (located in a sidebar)
+ */
+ foreach ( $sidebar_widget_ids as $i => $widget_id ) {
+ // Skip widgets that may have gone away due to a plugin being deactivated
+ if ( ! $is_active_sidebar || ! isset( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
+ continue;
+ }
+ $registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
+ $setting_id = self::get_setting_id( $widget_id );
+ $id_base = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
+ assert( false !== is_active_widget( $registered_widget['callback'], $registered_widget['id'], false, false ) );
+ $control = new WP_Widget_Form_Customize_Control(
+ $wp_customize,
+ $setting_id,
+ array(
+ 'label' => $registered_widget['name'],
+ 'section' => $section_id,
+ 'sidebar_id' => $sidebar_id,
+ 'widget_id' => $widget_id,
+ 'widget_id_base' => $id_base,
+ 'priority' => $i,
+ 'width' => $wp_registered_widget_controls[$widget_id]['width'],
+ 'height' => $wp_registered_widget_controls[$widget_id]['height'],
+ 'is_wide' => self::is_wide_widget( $widget_id ),
+ )
+ );
+ $wp_customize->add_control( $control );
+ }
+ }
+
+ /**
+ * We have to register these settings later than customize_preview_init so that other
+ * filters have had a chance to run.
+ * @see self::schedule_customize_register()
+ */
+ if ( did_action( 'customize_preview_init' ) ) {
+ foreach ( $new_setting_ids as $new_setting_id ) {
+ $wp_customize->get_setting( $new_setting_id )->preview();
+ }
+ }
+
+ self::remove_prepreview_filters();
+ }
+
+ /**
+ * Covert a widget_id into its corresponding customizer setting id (option name)
+ *
+ * @param string $widget_id
+ * @see _get_widget_id_base()
+ * @return string
+ */
+ static function get_setting_id( $widget_id ) {
+ $parsed_widget_id = self::parse_widget_id( $widget_id );
+ $setting_id = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
+ if ( ! is_null( $parsed_widget_id['number'] ) ) {
+ $setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
+ }
+ return $setting_id;
+ }
+
+ /**
+ * Core widgets which may have controls wider than 250, but can still be
+ * shown in the narrow customizer panel. The RSS and Text widgets in Core,
+ * for example, have widths of 400 and yet they still render fine in the
+ * customizer panel. This method will return all Core widgets as being
+ * not wide, but this can be overridden with the is_wide_widget_in_customizer
+ * filter.
+ *
+ * @param string $widget_id
+ * @return bool
+ */
+ static function is_wide_widget( $widget_id ) {
+ global $wp_registered_widget_controls;
+ $parsed_widget_id = self::parse_widget_id( $widget_id );
+ $width = $wp_registered_widget_controls[$widget_id]['width'];
+ $is_core = in_array( $parsed_widget_id['id_base'], self::$core_widget_id_bases );
+ $is_wide = ( $width > 250 && ! $is_core );
+ $is_wide = apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
+ return $is_wide;
+ }
+
+ /**
+ * Covert a widget ID into its id_base and number components
+ *
+ * @param string $widget_id
+ * @return array
+ */
+ static function parse_widget_id( $widget_id ) {
+ $parsed = array(
+ 'number' => null,
+ 'id_base' => null,
+ );
+ if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
+ $parsed['id_base'] = $matches[1];
+ $parsed['number'] = intval( $matches[2] );
+ } else {
+ // likely an old single widget
+ $parsed['id_base'] = $widget_id;
+ }
+ return $parsed;
+ }
+
+ /**
+ * Convert a widget setting ID (option path) to its id_base and number components
+ *
+ * @throws Widget_Customizer_Exception
+ * @throws Exception
+ *
+ * @param string $setting_id
+ * @param array
+ * @return array
+ */
+ static function parse_widget_setting_id( $setting_id ) {
+ if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
+ throw new Widget_Customizer_Exception( sprintf( 'Invalid widget setting ID: %s', $setting_id ) );
+ }
+ $id_base = $matches[2];
+ $number = isset( $matches[3] ) ? intval( $matches[3] ) : null;
+ return compact( 'id_base', 'number' );
+ }
+
+ /**
+ * Enqueue scripts and styles for customizer panel and export data to JS
+ *
+ * @action customize_controls_enqueue_scripts
+ */
+ static function customize_controls_enqueue_deps() {
+ wp_enqueue_script( 'jquery-ui-sortable' );
+ wp_enqueue_script( 'jquery-ui-droppable' );
+ wp_enqueue_style(
+ 'widget-customizer',
+ admin_url( 'css/customize-widgets.css' )
+ );
+ wp_enqueue_script(
+ 'widget-customizer',
+ admin_url( 'js/customize-widgets.js' ),
+ array( 'jquery', 'wp-backbone', 'wp-util', 'customize-controls' )
+ );
+
+ // Export available widgets with control_tpl removed from model
+ // since plugins need templates to be in the DOM
+ $available_widgets = array();
+ foreach ( self::get_available_widgets() as $available_widget ) {
+ unset( $available_widget['control_tpl'] );
+ $available_widgets[] = $available_widget;
+ }
+
+ $widget_reorder_nav_tpl = sprintf(
+ '<div class="widget-reorder-nav"><span class="move-widget" tabindex="0" title="%1$s">%2$s</span><span class="move-widget-down" tabindex="0" title="%3$s">%4$s</span><span class="move-widget-up" tabindex="0" title="%5$s">%6$s</span></div>',
+ esc_attr__( 'Move to another area...' ),
+ esc_html__( 'Move to another area...' ),
+ esc_attr__( 'Move down' ),
+ esc_html__( 'Move down' ),
+ esc_attr__( 'Move up' ),
+ esc_html__( 'Move up' )
+ );
+
+ $move_widget_area_tpl = str_replace(
+ array( '{description}', '{btn}' ),
+ array(
+ esc_html__( 'Select an area to move this widget into:' ),
+ esc_html__( 'Move' ),
+ ),
+ '
+ <div class="move-widget-area">
+ <p class="description">{description}</p>
+ <ul class="widget-area-select">
+ <% _.each( sidebars, function ( sidebar ){ %>
+ <li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
+ <% }); %>
+ </ul>
+ <div class="move-widget-actions">
+ <button class="move-widget-btn button-secondary" type="button">{btn}</button>
+ </div>
+ </div>
+ '
+ );
+
+ // Why not wp_localize_script? Because we're not localizing, and it forces values into strings
+ global $wp_scripts;
+ $exports = array(
+ 'update_widget_ajax_action' => self::UPDATE_WIDGET_AJAX_ACTION,
+ 'update_widget_nonce_value' => wp_create_nonce( self::UPDATE_WIDGET_AJAX_ACTION ),
+ 'update_widget_nonce_post_key' => self::UPDATE_WIDGET_NONCE_POST_KEY,
+ 'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
+ 'registered_widgets' => $GLOBALS['wp_registered_widgets'],
+ 'available_widgets' => $available_widgets, // @todo Merge this with registered_widgets
+ 'i18n' => array(
+ 'save_btn_label' => _x( 'Apply', 'button to save changes to a widget' ),
+ 'save_btn_tooltip' => _x( 'Save and preview changes before publishing them.', 'tooltip on the widget save button' ),
+ 'remove_btn_label' => _x( 'Remove', 'link to move a widget to the inactive widgets sidebar' ),
+ 'remove_btn_tooltip' => _x( 'Trash widget by moving it to the inactive widgets sidebar.', 'tooltip on btn a widget to move it to the inactive widgets sidebar' ),
+ ),
+ 'tpl' => array(
+ 'widget_reorder_nav' => $widget_reorder_nav_tpl,
+ 'move_widget_area' => $move_widget_area_tpl,
+ ),
+ );
+ foreach ( $exports['registered_widgets'] as &$registered_widget ) {
+ unset( $registered_widget['callback'] ); // may not be JSON-serializeable
+ }
+
+ $wp_scripts->add_data(
+ 'widget-customizer',
+ 'data',
+ sprintf( 'var WidgetCustomizer_exports = %s;', json_encode( $exports ) )
+ );
+ }
+
+ /**
+ * Render the widget form control templates into the DOM so that plugin scripts can manipulate them
+ *
+ * @action customize_controls_print_footer_scripts
+ */
+ static function output_widget_control_templates() {
+ ?>
+ <div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
+ <div id="available-widgets">
+ <div id="available-widgets-filter">
+ <input type="search" placeholder="<?php esc_attr_e( 'Find widgets…' ) ?>">
+ </div>
+ <?php foreach ( self::get_available_widgets() as $available_widget ): ?>
+ <div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ) ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ) ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ) ?>" tabindex="0">
+ <?php echo $available_widget['control_tpl']; // xss ok ?>
+ </div>
+ <?php endforeach; ?>
+ </div><!-- #available-widgets -->
+ </div><!-- #widgets-left -->
+ <?php
+ }
+
+ /**
+ * Get common arguments to supply when constructing a customizer setting
+ *
+ * @param string $id
+ * @param array [$overrides]
+ * @return array
+ */
+ static function get_setting_args( $id, $overrides = array() ) {
+ $args = array(
+ 'type' => 'option',
+ 'capability' => 'edit_theme_options',
+ 'transport' => 'refresh',
+ 'default' => array(),
+ );
+ $args = array_merge( $args, $overrides );
+ $args = apply_filters( 'widget_customizer_setting_args', $args, $id );
+ return $args;
+ }
+
+ /**
+ * Make sure that a sidebars_widgets[x] only ever consists of actual widget IDs.
+ * Used as sanitize_callback for each sidebars_widgets setting.
+ *
+ * @param array $widget_ids
+ * @return array
+ */
+ static function sanitize_sidebar_widgets( $widget_ids ) {
+ global $wp_registered_widgets;
+ $widget_ids = array_map( 'strval', (array) $widget_ids );
+ $sanitized_widget_ids = array();
+ foreach ( $widget_ids as $widget_id ) {
+ if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
+ $sanitized_widget_ids[] = $widget_id;
+ }
+ }
+ return $sanitized_widget_ids;
+ }
+
+ /**
+ * Special filter for Settings Revisions plugin until it can handle
+ * dynamically creating settings.
+ *
+ * @param mixed $value
+ * @param stdClass|WP_Customize_Setting $setting
+ * @return mixed
+ */
+ static function temp_customize_sanitize_js( $value, $setting ) {
+ if ( preg_match( '/^widget_/', $setting->id ) && $setting->type === 'option' ) {
+ $value = self::sanitize_widget_js_instance( $value );
+ }
+ return $value;
+ }
+
+ /**
+ * Build up an index of all available widgets for use in Backbone models
+ *
+ * @see wp_list_widgets()
+ * @return array
+ */
+ static function get_available_widgets() {
+ static $available_widgets = array();
+ if ( ! empty( $available_widgets ) ) {
+ return $available_widgets;
+ }
+
+ global $wp_registered_widgets, $wp_registered_widget_controls;
+ require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
+
+ $sort = $wp_registered_widgets;
+ usort( $sort, array( __CLASS__, '_sort_name_callback' ) );
+ $done = array();
+
+ foreach ( $sort as $widget ) {
+ if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
+ continue;
+ }
+
+ $sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
+ $done[] = $widget['callback'];
+
+ if ( ! isset( $widget['params'][0] ) ) {
+ $widget['params'][0] = array();
+ }
+
+ $available_widget = $widget;
+ unset( $available_widget['callback'] ); // not serializable to JSON
+
+ $args = array(
+ 'widget_id' => $widget['id'],
+ 'widget_name' => $widget['name'],
+ '_display' => 'template',
+ );
+
+ $is_disabled = false;
+ $is_multi_widget = (
+ isset( $wp_registered_widget_controls[$widget['id']]['id_base'] )
+ &&
+ isset( $widget['params'][0]['number'] )
+ );
+ if ( $is_multi_widget ) {
+ $id_base = $wp_registered_widget_controls[$widget['id']]['id_base'];
+ $args['_temp_id'] = "$id_base-__i__";
+ $args['_multi_num'] = next_widget_id_number( $id_base );
+ $args['_add'] = 'multi';
+ } else {
+ $args['_add'] = 'single';
+ if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
+ $is_disabled = true;
+ }
+ $id_base = $widget['id'];
+ }
+
+ $list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
+ $control_tpl = self::get_widget_control( $list_widget_controls_args );
+
+ // The properties here are mapped to the Backbone Widget model
+ $available_widget = array_merge(
+ $available_widget,
+ array(
+ 'temp_id' => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
+ 'is_multi' => $is_multi_widget,
+ 'control_tpl' => $control_tpl,
+ 'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
+ 'is_disabled' => $is_disabled,
+ 'id_base' => $id_base,
+ 'transport' => 'refresh',
+ 'width' => $wp_registered_widget_controls[$widget['id']]['width'],
+ 'height' => $wp_registered_widget_controls[$widget['id']]['height'],
+ 'is_wide' => self::is_wide_widget( $widget['id'] ),
+ )
+ );
+
+ $available_widgets[] = $available_widget;
+ }
+ return $available_widgets;
+ }
+
+ /**
+ * Replace with inline closure once on PHP 5.3:
+ * sort( $array, function ( $a, $b ) { return strnatcasecmp( $a['name'], $b['name'] ); } );
+ *
+ * @access private
+ */
+ static function _sort_name_callback( $a, $b ) {
+ return strnatcasecmp( $a['name'], $b['name'] );
+ }
+
+ /**
+ * Invoke wp_widget_control() but capture the output buffer and transform the markup
+ * so that it can be used in the customizer.
+ *
+ * @see wp_widget_control()
+ * @param array $args
+ * @return string
+ */
+ static function get_widget_control( $args ) {
+ ob_start();
+ call_user_func_array( 'wp_widget_control', $args );
+ $replacements = array(
+ '<form action="" method="post">' => '<div class="form">',
+ '</form>' => '</div><!-- .form -->',
+ );
+ $control_tpl = ob_get_clean();
+ $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
+ return $control_tpl;
+ }
+
+ /**
+ * Add hooks for the customizer preview
+ *
+ * @action customize_preview_init
+ */
+ static function customize_preview_init() {
+ add_filter( 'sidebars_widgets', array( __CLASS__, 'preview_sidebars_widgets' ), 1 );
+ add_action( 'wp_enqueue_scripts', array( __CLASS__, 'customize_preview_enqueue_deps' ) );
+ add_action( 'wp_footer', array( __CLASS__, 'export_preview_data' ), 9999 );
+ }
+
+ /**
+ * When previewing, make sure the proper previewing widgets are used. Because wp_get_sidebars_widgets()
+ * gets called early at init (via wp_convert_widget_settings()) and can set global variable
+ * $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' ) before the customizer
+ * preview filter is added, we have to reset it after the filter has been added.
+ *
+ * @filter sidebars_widgets
+ */
+ static function preview_sidebars_widgets( $sidebars_widgets ) {
+ $sidebars_widgets = get_option( 'sidebars_widgets' );
+ unset( $sidebars_widgets['array_version'] );
+ return $sidebars_widgets;
+ }
+
+ /**
+ * Enqueue scripts for the customizer preview
+ *
+ * @action wp_enqueue_scripts
+ */
+ static function customize_preview_enqueue_deps() {
+ wp_enqueue_script(
+ 'customize-preview-widgets',
+ includes_url( 'js/customize-preview-widgets.js' ),
+ array( 'jquery', 'wp-util', 'customize-preview' )
+ );
+
+ /*
+ wp_enqueue_style(
+ 'widget-customizer-preview',
+ 'widget-customizer-preview.css'
+ );
+ */
+
+ // Why not wp_localize_script? Because we're not localizing, and it forces values into strings
+ global $wp_scripts;
+ $exports = array(
+ 'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
+ 'registered_widgets' => $GLOBALS['wp_registered_widgets'],
+ 'i18n' => array(
+ 'widget_tooltip' => __( 'Press shift and then click to edit widget in customizer...' ),
+ ),
+ 'request_uri' => wp_unslash( $_SERVER['REQUEST_URI'] ),
+ );
+ foreach ( $exports['registered_widgets'] as &$registered_widget ) {
+ unset( $registered_widget['callback'] ); // may not be JSON-serializeable
+ }
+ $wp_scripts->add_data(
+ 'customize-preview-widgets',
+ 'data',
+ sprintf( 'var WidgetCustomizerPreview_exports = %s;', json_encode( $exports ) )
+ );
+ }
+
+ /**
+ * At the very end of the page, at the very end of the wp_footer, communicate the sidebars that appeared on the page
+ *
+ * @action wp_footer
+ */
+ static function export_preview_data() {
+ wp_print_scripts( array( 'customize-preview-widgets' ) );
+ ?>
+ <script>
+ (function () {
+ /*global WidgetCustomizerPreview */
+ WidgetCustomizerPreview.rendered_sidebars = <?php echo json_encode( array_fill_keys( array_unique( self::$rendered_sidebars ), true ) ) ?>;
+ WidgetCustomizerPreview.rendered_widgets = <?php echo json_encode( array_fill_keys( array_keys( self::$rendered_widgets ), true ) ); ?>;
+ }());
+ </script>
+ <?php
+ }
+
+ static protected $rendered_sidebars = array();
+ static protected $rendered_widgets = array();
+
+ /**
+ * Keep track of the widgets that were rendered
+ *
+ * @action dynamic_sidebar
+ */
+ static function tally_rendered_widgets( $widget ) {
+ self::$rendered_widgets[$widget['id']] = true;
+ }
+
+ /**
+ * This is hacky. It is too bad that dynamic_sidebar is not just called once with the $sidebar_id supplied
+ * This does not get called for a sidebar which lacks widgets.
+ * See core patch which addresses the problem.
+ *
+ * @link http://core.trac.wordpress.org/ticket/25368
+ * @action dynamic_sidebar
+ */
+ static function tally_sidebars_via_dynamic_sidebar_actions( $widget ) {
+ global $sidebars_widgets;
+ foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) {
+ if ( in_array( $sidebar_id, self::$rendered_sidebars ) ) {
+ continue;
+ }
+ if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) && is_array( $widget_ids ) && in_array( $widget['id'], $widget_ids ) ) {
+ self::$rendered_sidebars[] = $sidebar_id;
+ }
+ }
+ }
+
+ /**
+ * Keep track of the times that is_active_sidebar() is called in the template, and assume that this
+ * means that the sidebar would be rendered on the template if there were widgets populating it.
+ *
+ * @see http://core.trac.wordpress.org/ticket/25368
+ * @filter temp_is_active_sidebar
+ */
+ static function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
+ if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
+ self::$rendered_sidebars[] = $sidebar_id;
+ }
+ // We may need to force this to true, and also force-true the value for temp_dynamic_sidebar_has_widgets
+ // if we want to ensure that there is an area to drop widgets into, if the sidebar is empty.
+ return $is_active;
+ }
+
+ /**
+ * Keep track of the times that dynamic_sidebar() is called in the template, and assume that this
+ * means that the sidebar would be rendered on the template if there were widgets populating it.
+ *
+ * @see http://core.trac.wordpress.org/ticket/25368
+ * @filter temp_dynamic_sidebar_has_widgets
+ */
+ static function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
+ if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
+ self::$rendered_sidebars[] = $sidebar_id;
+ }
+ // We may need to force this to true, and also force-true the value for temp_is_active_sidebar
+ // if we want to ensure that there is an area to drop widgets into, if the sidebar is empty.
+ return $has_widgets;
+ }
+
+ /**
+ * Serialize an instance and hash it with the AUTH_KEY; when a JS value is
+ * posted back to save, this instance hash key is used to ensure that the
+ * serialized_instance was not tampered with, but that it had originated
+ * from WordPress and so is sanitized.
+ *
+ * @param array $instance
+ * @return string
+ */
+ protected static function get_instance_hash_key( $instance ) {
+ $hash = md5( AUTH_KEY . serialize( $instance ) );
+ return $hash;
+ }
+
+ /**
+ * Unserialize the JS-instance for storing in the options. It's important
+ * that this filter only get applied to an instance once.
+ *
+ * @see Widget_Customizer::sanitize_widget_js_instance()
+ *
+ * @param array $value
+ * @return array
+ */
+ static function sanitize_widget_instance( $value ) {
+ if ( $value === array() ) {
+ return $value;
+ }
+ $invalid = (
+ empty( $value['is_widget_customizer_js_value'] )
+ ||
+ empty( $value['instance_hash_key'] )
+ ||
+ empty( $value['encoded_serialized_instance'] )
+ );
+ if ( $invalid ) {
+ return null;
+ }
+ $decoded = base64_decode( $value['encoded_serialized_instance'], true );
+ if ( false === $decoded ) {
+ return null;
+ }
+ $instance = unserialize( $decoded );
+ if ( false === $instance ) {
+ return null;
+ }
+ if ( self::get_instance_hash_key( $instance ) !== $value['instance_hash_key'] ) {
+ return null;
+ }
+ return $instance;
+ }
+
+ /**
+ * Convert widget instance into JSON-representable format
+ *
+ * @see Widget_Customizer::sanitize_widget_instance()
+ *
+ * @param array $value
+ * @return array
+ */
+ static function sanitize_widget_js_instance( $value ) {
+ if ( empty( $value['is_widget_customizer_js_value'] ) ) {
+ $serialized = serialize( $value );
+ $value = array(
+ 'encoded_serialized_instance' => base64_encode( $serialized ),
+ 'title' => empty( $value['title'] ) ? '' : $value['title'],
+ 'is_widget_customizer_js_value' => true,
+ 'instance_hash_key' => self::get_instance_hash_key( $value ),
+ );
+ }
+ return $value;
+ }
+
+ /**
+ * Strip out widget IDs for widgets which are no longer registered, such
+ * as the case when a plugin orphans a widget in a sidebar when it is deactivated.
+ *
+ * @param array $widget_ids
+ * @return array
+ */
+ static function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
+ global $wp_registered_widgets;
+ $widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
+ return $widget_ids;
+ }
+
+ /**
+ * Find and invoke the widget update and control callbacks. Requires that
+ * $_POST be populated with the instance data.
+ *
+ * @throws Widget_Customizer_Exception
+ * @throws Exception
+ *
+ * @param string $widget_id
+ * @return array
+ */
+ static function call_widget_update( $widget_id ) {
+ global $wp_registered_widget_updates, $wp_registered_widget_controls;
+
+ $options_transaction = new Options_Transaction();
+
+ try {
+ $options_transaction->start();
+ $parsed_id = self::parse_widget_id( $widget_id );
+ $option_name = 'widget_' . $parsed_id['id_base'];
+
+ /**
+ * If a previously-sanitized instance is provided, populate the input vars
+ * with its values so that the widget update callback will read this instance
+ */
+ $added_input_vars = array();
+ if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
+ $sanitized_widget_setting = json_decode( self::get_post_value( 'sanitized_widget_setting' ), true );
+ if ( empty( $sanitized_widget_setting ) ) {
+ throw new Widget_Customizer_Exception( 'Malformed sanitized_widget_setting' );
+ }
+ $instance = self::sanitize_widget_instance( $sanitized_widget_setting );
+ if ( is_null( $instance ) ) {
+ throw new Widget_Customizer_Exception( 'Unsanitary sanitized_widget_setting' );
+ }
+ if ( ! is_null( $parsed_id['number'] ) ) {
+ $value = array();
+ $value[$parsed_id['number']] = $instance;
+ $key = 'widget-' . $parsed_id['id_base'];
+ $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
+ $added_input_vars[] = $key;
+ } else {
+ foreach ( $instance as $key => $value ) {
+ $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
+ $added_input_vars[] = $key;
+ }
+ }
+ }
+
+ /**
+ * Invoke the widget update callback
+ */
+ foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
+ if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
+ ob_start();
+ call_user_func_array( $control['callback'], $control['params'] );
+ ob_end_clean();
+ break;
+ }
+ }
+
+ // Clean up any input vars that were manually added
+ foreach ( $added_input_vars as $key ) {
+ unset( $_POST[$key] );
+ unset( $_REQUEST[$key] );
+ }
+
+ /**
+ * Make sure the expected option was updated
+ */
+ if ( 0 !== $options_transaction->count() ) {
+ if ( count( $options_transaction->options ) > 1 ) {
+ throw new Widget_Customizer_Exception( sprintf( 'Widget %1$s unexpectedly updated more than one option.', $widget_id ) );
+ }
+ $updated_option_name = key( $options_transaction->options );
+ if ( $updated_option_name !== $option_name ) {
+ throw new Widget_Customizer_Exception( sprintf( 'Widget %1$s updated option "%2$s", but expected "%3$s".', $widget_id, $updated_option_name, $option_name ) );
+ }
+ }
+
+ /**
+ * Obtain the widget control with the updated instance in place
+ */
+ ob_start();
+ $form = $wp_registered_widget_controls[$widget_id];
+ if ( $form ) {
+ call_user_func_array( $form['callback'], $form['params'] );
+ }
+ $form = ob_get_clean();
+
+ /**
+ * Obtain the widget instance
+ */
+ $option = get_option( $option_name );
+ if ( null !== $parsed_id['number'] ) {
+ $instance = $option[$parsed_id['number']];
+ } else {
+ $instance = $option;
+ }
+
+ $options_transaction->rollback();
+ return compact( 'instance', 'form' );
+ }
+ catch ( Exception $e ) {
+ $options_transaction->rollback();
+ throw $e;
+ }
+ }
+
+ /**
+ * Allow customizer to update a widget using its form, but return the new
+ * instance info via Ajax instead of saving it to the options table.
+ * Most code here copied from wp_ajax_save_widget()
+ *
+ * @see wp_ajax_save_widget
+ * @todo Reuse wp_ajax_save_widget now that we have option transactions?
+ * @action wp_ajax_update_widget
+ */
+ static function wp_ajax_update_widget() {
+ $generic_error = __( 'An error has occurred. Please reload the page and try again.' );
+
+ try {
+ if ( ! check_ajax_referer( self::UPDATE_WIDGET_AJAX_ACTION, self::UPDATE_WIDGET_NONCE_POST_KEY, false ) ) {
+ throw new Widget_Customizer_Exception( __( 'Nonce check failed. Reload and try again?' ) );
+ }
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ throw new Widget_Customizer_Exception( __( 'Current user cannot!' ) );
+ }
+ if ( ! isset( $_POST['widget-id'] ) ) {
+ throw new Widget_Customizer_Exception( __( 'Incomplete request' ) );
+ }
+
+ unset( $_POST[self::UPDATE_WIDGET_NONCE_POST_KEY], $_POST['action'] );
+
+ do_action( 'load-widgets.php' );
+ do_action( 'widgets.php' );
+ do_action( 'sidebar_admin_setup' );
+
+ $widget_id = self::get_post_value( 'widget-id' );
+ $parsed_id = self::parse_widget_id( $widget_id );
+ $id_base = $parsed_id['id_base'];
+
+ if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
+ throw new Widget_Customizer_Exception( 'Cannot pass widget templates to create new instances; apply template vars in JS' );
+ }
+
+ $updated_widget = self::call_widget_update( $widget_id ); // => {instance,form}
+ $form = $updated_widget['form'];
+ $instance = self::sanitize_widget_js_instance( $updated_widget['instance'] );
+
+ wp_send_json_success( compact( 'form', 'instance' ) );
+ }
+ catch( Exception $e ) {
+ if ( $e instanceof Widget_Customizer_Exception ) {
+ $message = $e->getMessage();
+ } else {
+ error_log( sprintf( '%s in %s: %s', get_class( $e ), __FUNCTION__, $e->getMessage() ) );
+ $message = $generic_error;
+ }
+ wp_send_json_error( compact( 'message' ) );
+ }
+ }
+}
+
+class Widget_Customizer_Exception extends Exception {}
+
+class Options_Transaction {
+
+ /**
+ * @var array $options values updated while transaction is open
+ */
+ public $options = array();
+
+ protected $_ignore_transients = true;
+ protected $_is_current = false;
+ protected $_operations = array();
+
+ function __construct( $ignore_transients = true ) {
+ $this->_ignore_transients = $ignore_transients;
+ }
+
+ /**
+ * Determine whether or not the transaction is open
+ * @return bool
+ */
+ function is_current() {
+ return $this->_is_current;
+ }
+
+ /**
+ * @param $option_name
+ * @return boolean
+ */
+ function is_option_ignored( $option_name ) {
+ return ( $this->_ignore_transients && 0 === strpos( $option_name, '_transient_' ) );
+ }
+
+ /**
+ * Get the number of operations performed in the transaction
+ * @return bool
+ */
+ function count() {
+ return count( $this->_operations );
+ }
+
+ /**
+ * Start keeping track of changes to options, and cache their new values
+ */
+ function start() {
+ $this->_is_current = true;
+ add_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
+ add_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
+ add_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
+ add_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
+ }
+
+ /**
+ * @action added_option
+ * @param $option_name
+ * @param $new_value
+ */
+ function _capture_added_option( $option_name, $new_value ) {
+ if ( $this->is_option_ignored( $option_name ) ) {
+ return;
+ }
+ $this->options[$option_name] = $new_value;
+ $operation = 'add';
+ $this->_operations[] = compact( 'operation', 'option_name', 'new_value' );
+ }
+
+ /**
+ * @action updated_option
+ * @param string $option_name
+ * @param mixed $old_value
+ * @param mixed $new_value
+ */
+ function _capture_updated_option( $option_name, $old_value, $new_value ) {
+ if ( $this->is_option_ignored( $option_name ) ) {
+ return;
+ }
+ $this->options[$option_name] = $new_value;
+ $operation = 'update';
+ $this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'new_value' );
+ }
+
+ protected $_pending_delete_option_autoload;
+ protected $_pending_delete_option_value;
+
+ /**
+ * It's too bad the old_value and autoload aren't passed into the deleted_option action
+ * @action delete_option
+ * @param string $option_name
+ */
+ function _capture_pre_deleted_option( $option_name ) {
+ if ( $this->is_option_ignored( $option_name ) ) {
+ return;
+ }
+ global $wpdb;
+ $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // db call ok; no-cache ok
+ $this->_pending_delete_option_autoload = $autoload;
+ $this->_pending_delete_option_value = get_option( $option_name );
+ }
+
+ /**
+ * @action deleted_option
+ * @param string $option_name
+ */
+ function _capture_deleted_option( $option_name ) {
+ if ( $this->is_option_ignored( $option_name ) ) {
+ return;
+ }
+ unset( $this->options[$option_name] );
+ $operation = 'delete';
+ $old_value = $this->_pending_delete_option_value;
+ $autoload = $this->_pending_delete_option_autoload;
+ $this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'autoload' );
+ }
+
+ /**
+ * Undo any changes to the options since start() was called
+ */
+ function rollback() {
+ remove_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
+ remove_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
+ remove_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
+ remove_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
+ while ( 0 !== count( $this->_operations ) ) {
+ $option_operation = array_pop( $this->_operations );
+ if ( 'add' === $option_operation['operation'] ) {
+ delete_option( $option_operation['option_name'] );
+ }
+ else if ( 'delete' === $option_operation['operation'] ) {
+ add_option( $option_operation['option_name'], $option_operation['old_value'], null, $option_operation['autoload'] );
+ }
+ else if ( 'update' === $option_operation['operation'] ) {
+ update_option( $option_operation['option_name'], $option_operation['old_value'] );
+ }
+ else {
+ throw new Exception( 'Unexpected operation' );
+ }
+ }
+ $this->_is_current = false;
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesjscustomizepreviewwidgetsjs"></a>
<div class="addfile"><h4>Added: trunk/src/wp-includes/js/customize-preview-widgets.js (0 => 27419)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/js/customize-preview-widgets.js (rev 0)
+++ trunk/src/wp-includes/js/customize-preview-widgets.js 2014-03-05 20:40:36 UTC (rev 27419)
</span><span class="lines">@@ -0,0 +1,115 @@
</span><ins>+/*global jQuery, WidgetCustomizerPreview_exports */
+/*exported WidgetCustomizerPreview */
+var WidgetCustomizerPreview = (function ($) {
+ 'use strict';
+
+ var self = {
+ rendered_sidebars: {}, // @todo Make rendered a property of the Backbone model
+ rendered_widgets: {}, // @todo Make rendered a property of the Backbone model
+ registered_sidebars: [], // @todo Make a Backbone collection
+ registered_widgets: {}, // @todo Make array, Backbone collection
+ widget_selectors: [],
+ render_widget_ajax_action: null,
+ render_widget_nonce_value: null,
+ render_widget_nonce_post_key: null,
+ preview: null,
+ i18n: {},
+
+ init: function () {
+ this.buildWidgetSelectors();
+ this.highlightControls();
+
+ self.preview.bind( 'active', function() {
+ self.preview.send( 'rendered-sidebars', self.rendered_sidebars ); // @todo Only send array of IDs
+ self.preview.send( 'rendered-widgets', self.rendered_widgets ); // @todo Only send array of IDs
+ } );
+ },
+
+ /**
+ * Calculate the selector for the sidebar's widgets based on the registered sidebar's info
+ */
+ buildWidgetSelectors: function () {
+ $.each( self.registered_sidebars, function ( i, sidebar ) {
+ var widget_tpl = [
+ sidebar.before_widget.replace('%1$s', '').replace('%2$s', ''),
+ sidebar.before_title,
+ sidebar.after_title,
+ sidebar.after_widget
+ ].join('');
+ var empty_widget = $(widget_tpl);
+ var widget_selector = empty_widget.prop('tagName');
+ var widget_classes = empty_widget.prop('className').replace(/^\s+|\s+$/g, '');
+ if ( widget_classes ) {
+ widget_selector += '.' + widget_classes.split(/\s+/).join('.');
+ }
+ self.widget_selectors.push(widget_selector);
+ });
+ },
+
+ /**
+ * Obtain a widget instance if it was added to the provided sidebar
+ * This addresses a race condition where a widget is moved between sidebars
+ * We cannot use ID selector because jQuery will only return the first one
+ * that matches. We have to resort to an [id] attribute selector
+ *
+ * @param {String} sidebar_id
+ * @param {String} widget_id
+ * @return {jQuery}
+ */
+ getSidebarWidgetElement: function ( sidebar_id, widget_id ) {
+ return $( '[id=' + widget_id + ']' ).filter( function () {
+ return $( this ).data( 'widget_customizer_sidebar_id' ) === sidebar_id;
+ } );
+ },
+
+ /**
+ *
+ */
+ highlightControls: function() {
+
+ var selector = this.widget_selectors.join(',');
+
+ $(selector).attr( 'title', self.i18n.widget_tooltip );
+
+ $(document).on( 'mouseenter', selector, function () {
+ var control = parent.WidgetCustomizer.getWidgetFormControlForWidget( $(this).prop('id') );
+ if ( control ) {
+ control.highlightSectionAndControl();
+ }
+ });
+
+ // Open expand the widget control when shift+clicking the widget element
+ $(document).on( 'click', selector, function ( e ) {
+ if ( ! e.shiftKey ) {
+ return;
+ }
+ e.preventDefault();
+ var control = parent.WidgetCustomizer.getWidgetFormControlForWidget( $(this).prop('id') );
+ if ( control ) {
+ control.focus();
+ }
+ });
+ }
+
+ };
+
+ $.extend(self, WidgetCustomizerPreview_exports);
+
+ /**
+ * Capture the instance of the Preview since it is private
+ */
+ var OldPreview = wp.customize.Preview;
+ wp.customize.Preview = OldPreview.extend( {
+ initialize: function( params, options ) {
+ self.preview = this;
+ OldPreview.prototype.initialize.call( this, params, options );
+ }
+ } );
+
+ // @todo on customize ready?
+ $(function () {
+ self.init();
+ });
+
+ return self;
+}( jQuery ));
</ins></span></pre>
</div>
</div>
</body>
</html>