diff --git a/wp-admin/admin-ajax.php b/wp-admin/admin-ajax.php
index d57d56b1d4..838877efc8 100644
--- a/wp-admin/admin-ajax.php
+++ b/wp-admin/admin-ajax.php
@@ -58,7 +58,7 @@ $core_actions_post = array(
'wp-remove-post-lock', 'dismiss-wp-pointer', 'upload-attachment', 'get-attachment',
'query-attachments', 'save-attachment', 'save-attachment-compat', 'send-link-to-editor',
'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs',
- 'save-user-color-scheme',
+ 'save-user-color-scheme', 'update-widget',
);
// Register core Ajax calls.
diff --git a/wp-admin/css/customize-widgets-rtl.css b/wp-admin/css/customize-widgets-rtl.css
new file mode 100644
index 0000000000..124a7aebdf
--- /dev/null
+++ b/wp-admin/css/customize-widgets-rtl.css
@@ -0,0 +1,623 @@
+.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
+ */
+ right: auto !important;
+ max-width: 100%;
+}
+.customize-control-widget_form.wide-widget-control .widget-inside {
+ position: fixed;
+ right: 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: left;
+ padding: 5px 10px;
+ margin-left: 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: left;
+ background-color: #fafafa;
+}
+
+.widget-reorder-nav span {
+ position: relative;
+ overflow: hidden;
+ float: right;
+ 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;
+ left: 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 42px 15px 15px;
+ 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;
+ right: 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: left;
+ 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 {
+ left: 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: left;
+ -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;
+ right: -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;
+ right: -301px;
+ width: 300px;
+ margin: 0;
+ z-index: 1;
+ background: #fff;
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+ border-left: 1px solid #dddddd;
+}
+
+#available-widgets-filter {
+ padding: 8px 13px 7px 17px;
+ 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 60px 20px 15px;
+ 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 {
+ right: 0;
+}
+
+body.adding-widget .wp-full-overlay-main {
+ right: 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;
+ left: 100%;
+ margin-left: 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 42px 11px 15px;
+ }
+ #customize-theme-controls .widget-area-select li:before {
+ top: 6px;
+ }
+}
diff --git a/wp-admin/css/customize-widgets.css b/wp-admin/css/customize-widgets.css
new file mode 100644
index 0000000000..6064165d81
--- /dev/null
+++ b/wp-admin/css/customize-widgets.css
@@ -0,0 +1,623 @@
+.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;
+ }
+}
diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php
index d89de1a06b..788bd0cf0b 100644
--- a/wp-admin/includes/ajax-actions.php
+++ b/wp-admin/includes/ajax-actions.php
@@ -1587,6 +1587,13 @@ function wp_ajax_save_widget() {
wp_die();
}
+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();
+}
+
function wp_ajax_upload_attachment() {
check_ajax_referer( 'media-form' );
diff --git a/wp-admin/js/customize-widgets.js b/wp-admin/js/customize-widgets.js
new file mode 100644
index 0000000000..290142d481
--- /dev/null
+++ b/wp-admin/js/customize-widgets.js
@@ -0,0 +1,1733 @@
+/*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 = $( '
' );
+ 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
+ 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
+ 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 = $( '' + r.data.form + '
' );
+ 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);
diff --git a/wp-admin/js/customize-widgets.min.js b/wp-admin/js/customize-widgets.min.js
new file mode 100644
index 0000000000..ac73463aa3
--- /dev/null
+++ b/wp-admin/js/customize-widgets.min.js
@@ -0,0 +1 @@
+var WidgetCustomizer=function(a){"use strict";function b(a){var b={number:null,id_base:null},c=a.match(/^(.+)-(\d+)$/);return c?(b.id_base=c[1],b.number=parseInt(c[2],10)):b.id_base=a,b}function c(a){var c=b(a),d="widget_"+c.id_base;return c.number&&(d+="["+c.number+"]"),d}var d=wp.customize,e={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:[],registered_widgets:[],active_sidebar_control:null,previewer:null,saved_widget_ids:{},registered_sidebars:[],tpl:{move_widget_area:"",widget_reorder_nav:""}};a.extend(e,WidgetCustomizer_exports),"undefined"==typeof window.ajaxurl&&(window.ajaxurl=wp.ajax.settings.url),a("#customize-theme-controls").closest("div:not([id])").attr("id","widgets-right");var f=e.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}),g=e.WidgetCollection=Backbone.Collection.extend({model:f});e.available_widgets=new g(e.available_widgets);var h=e.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:!1}),i=e.SidebarCollection=Backbone.Collection.extend({model:h});e.registered_sidebars=new i(e.registered_sidebars),e.init=function(){this.showFirstSidebarIfRequested(),this.availableWidgetsPanel.setup()},wp.customize.bind("ready",function(){e.init()}),e.showFirstSidebarIfRequested=function(){if(/widget-customizer=open/.test(location.search)){var b=function(){e.registered_sidebars.off("change:is_rendered",b);var c=e.registered_sidebars.find(function(a){return a.get("is_rendered")});if(c){var d=a("#accordion-section-sidebar-widgets-"+c.get("id"));d.hasClass("open")||d.find(".accordion-section-title").trigger("click"),d[0].scrollIntoView()}};b=_.debounce(b,100),e.registered_sidebars.on("change:is_rendered",b)}},d.controlConstructor.sidebar_widgets=d.Control.extend({ready:function(){var a=this;a.control_section=a.container.closest(".control-section"),a.section_content=a.container.closest(".accordion-section-content"),a._setupModel(),a._setupSortable(),a._setupAddition(),a._applyCardinalOrderClassNames()},_setupModel:function(){var c=this,d=e.registered_sidebars.get(c.params.sidebar_id);c.setting.bind(function(d,f){var g=_(f).difference(d);d=_(d).filter(function(a){var c=b(a);return!!e.available_widgets.findWhere({id_base:c.id_base})});var h=_(d).map(function(a){var b=e.getWidgetFormControlForWidget(a);return b||(b=c.addWidget(a)),b});h.sort(function(a,b){var c=d.indexOf(a.params.widget_id),e=d.indexOf(b.params.widget_id);return c===e?0:e>c?-1:1});var i=c.section_content.find(".customize-control-sidebar_widgets"),j=_(h).map(function(a){return a.container[0]});i.before(j),c._applyCardinalOrderClassNames(),_(h).each(function(a){a.params.sidebar_id=c.params.sidebar_id}),_(g).each(function(d){setTimeout(function(){var f=!1;if(wp.customize.each(function(a){if(a.id!==c.setting.id&&0===a.id.indexOf("sidebars_widgets[")&&"sidebars_widgets[wp_inactive_widgets]"!==a.id){var b=a(),e=b.indexOf(d);-1!==e&&(f=!0)}}),!f){var g=e.getWidgetFormControlForWidget(d),h=g&&a.contains(document,g.container[0])&&!a.contains(c.section_content[0],g.container[0]);if(g&&!h&&(wp.customize.control.remove(g.id),g.container.remove()),e.saved_widget_ids[d]){var i=wp.customize.value("sidebars_widgets[wp_inactive_widgets]")().slice();i.push(d),wp.customize.value("sidebars_widgets[wp_inactive_widgets]")(_(i).unique())}var j=b(d).id_base,k=e.available_widgets.findWhere({id_base:j});k&&!k.get("is_multi")&&k.set("is_disabled",!1)}})})}),e.previewer.bind("rendered-sidebars",function(a){var b=!!a[c.params.sidebar_id];d.set("is_rendered",b)}),d.on("change:is_rendered",function(){var b="#accordion-section-sidebar-widgets-"+this.get("id"),c=a(b);this.get("is_rendered")?c.stop().slideDown(function(){a(this).css("height","auto")}):(c.hasClass("open")&&c.find(".accordion-section-title").trigger("click"),c.stop().slideUp())})},_setupSortable:function(){var b=this;b.is_reordering=!1,b.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 c=b.section_content.sortable("toArray"),d=a.map(c,function(b){return a("#"+b).find(":input[name=widget-id]").val()});b.setting(d)}}),b.control_section.find(".accordion-section-title").droppable({accept:".customize-control-widget_form",over:function(){b.control_section.hasClass("open")||(b.control_section.addClass("open"),b.section_content.toggle(!1).slideToggle(150,function(){b.section_content.sortable("refreshPositions")}))}}),b.container.find(".reorder-toggle").on("click keydown",function(a){("keydown"!==a.type||13===a.which||32===a.which)&&b.toggleReordering(!b.is_reordering)})},_setupAddition:function(){var b=this;b.container.find(".add-new-widget").on("click keydown",function(c){("keydown"!==c.type||13===c.which||32===c.which)&&(b.section_content.hasClass("reordering")||(a("body").hasClass("adding-widget")?e.availableWidgetsPanel.close():e.availableWidgetsPanel.open(b)))})},_applyCardinalOrderClassNames:function(){var a=this;a.section_content.find(".customize-control-widget_form").removeClass("first-widget").removeClass("last-widget").find(".move-widget-down, .move-widget-up").prop("tabIndex",0),a.section_content.find(".customize-control-widget_form:first").addClass("first-widget").find(".move-widget-up").prop("tabIndex",-1),a.section_content.find(".customize-control-widget_form:last").addClass("last-widget").find(".move-widget-down").prop("tabIndex",-1)},toggleReordering:function(a){var b=this;a=Boolean(a),a!==b.section_content.hasClass("reordering")&&(b.is_reordering=a,b.section_content.toggleClass("reordering",a),a&&_(b.getWidgetFormControls()).each(function(a){a.collapseForm()}))},getWidgetFormControls:function(){var a=this,b=_(a.setting()).map(function(a){var b=c(a),e=d.control(b);if(!e)throw new Error("Unable to find widget_form control for "+a);return e});return b},addWidget:function(c){var d=this,f=b(c),g=f.number,h=f.id_base,i=e.available_widgets.findWhere({id_base:h});if(!i)throw new Error("Widget unexpectedly not found.");if(g&&!i.get("is_multi"))throw new Error("Did not expect a widget number to be supplied for a non-multi widget");i.get("is_multi")&&!g&&(i.set("multi_number",i.get("multi_number")+1),g=i.get("multi_number"));var j=a("#widget-tpl-"+i.get("id")).html();i.get("is_multi")?j=j.replace(/<[^<>]+>/g,function(a){return a.replace(/__i__|%i%/g,g)}):i.set("is_disabled",!0);var k="widget_form",l=a("");l.addClass("customize-control"),l.addClass("customize-control-"+k),l.append(a(j)),l.find("> .widget-icon").remove(),i.get("is_multi")&&(l.find('input[name="widget_number"]').val(g),l.find('input[name="multi_number"]').val(g)),c=l.find('[name="widget-id"]').val(),l.hide();var m="widget_"+i.get("id_base");i.get("is_multi")&&(m+="["+g+"]"),l.attr("id","customize-control-"+m.replace(/\]/g,"").replace(/\[/g,"-")),d.container.after(l);var n=wp.customize.has(m);if(!n){var o={transport:"refresh",previewer:d.setting.previewer};wp.customize.create(m,m,{},o)}var p=wp.customize.controlConstructor[k],q=new p(m,{params:{settings:{"default":m},sidebar_id:d.params.sidebar_id,widget_id:c,widget_id_base:i.get("id_base"),type:k,is_new:!n,width:i.get("width"),height:i.get("height"),is_wide:i.get("is_wide")},previewer:d.setting.previewer});wp.customize.control.add(m,q),wp.customize.each(function(a){if(a.id!==d.setting.id&&0===a.id.indexOf("sidebars_widgets[")){var b=a().slice(),e=b.indexOf(c);-1!==e&&(b.splice(e),a(b))}});var r=d.setting().slice();return-1===r.indexOf(c)&&(r.push(c),d.setting(r)),l.slideDown(function(){n?(q.expandForm(),q.updateWidget({instance:q.setting(),complete:function(a){if(a)throw a;q.focus()}})):q.focus()}),q}}),d.controlConstructor.widget_form=d.Control.extend({ready:function(){var a=this;a._setupModel(),a._setupWideWidget(),a._setupControlToggle(),a._setupWidgetTitle(),a._setupReorderUI(),a._setupHighlightEffects(),a._setupUpdateUI(),a._setupRemoveUI(),a.hook("init")},hooks:{_default:{},rss:{formUpdated:function(a){var b=this,c=b.container.find(".widget-error:first"),d=a.find(".widget-error:first");c.length&&d.length?c.replaceWith(d):c.length?c.remove():d.length&&b.container.find(".widget-content").prepend(d)}}},hook:function(a){var b,c=Array.prototype.slice.call(arguments,1);this.hooks[this.params.widget_id_base]&&this.hooks[this.params.widget_id_base][a]?b=this.hooks[this.params.widget_id_base][a]:this.hooks._default[a]&&(b=this.hooks._default[a]),b&&b.apply(this,c)},_setupModel:function(){var a=this,b=function(){e.saved_widget_ids[a.params.widget_id]=!0};wp.customize.bind("ready",b),wp.customize.bind("saved",b),a._update_count=0,a.is_widget_updating=!1,a.setting.bind(function(b,c){_(c).isEqual(b)||a.is_widget_updating||a.updateWidget({instance:b})})},_setupWideWidget:function(){var b=this;if(b.params.is_wide){var c=b.container.find(".widget-inside"),d=a(".wp-full-overlay-sidebar-content:first");b.container.addClass("wide-widget-control"),b.container.find(".widget-content:first").css({"min-width":b.params.width,"min-height":b.params.height});var e=function(){var d=b.container.offset().top,e=c.outerHeight(),f=Math.max(d,0),g=a(window).height()-e;f=Math.min(f,g),c.css("top",f)},f=a("#customize-theme-controls");b.container.on("expand",function(){d.on("scroll",e),a(window).on("resize",e),f.on("expanded collapsed",e),e()}),b.container.on("collapsed",function(){d.off("scroll",e),f.off("expanded collapsed",e),a(window).off("resize",e)}),wp.customize.each(function(a){0===a.id.indexOf("sidebars_widgets[")&&a.bind(function(){b.container.hasClass("expanded")&&e()})})}},_setupControlToggle:function(){var a=this;a.container.find(".widget-top").on("click",function(b){b.preventDefault();var c=a.getSidebarWidgetsControl();c.is_reordering||a.toggleForm()});var b=a.container.find(".widget-control-close");b.on("click",function(b){b.preventDefault(),a.collapseForm(),a.container.find(".widget-top .widget-action:first").focus()})},_setupWidgetTitle:function(){var a=this,b=function(){var b=a.setting().title,c=a.container.find(".in-widget-title");c.text(b?": "+b:"")};a.setting.bind(b),b()},_setupReorderUI:function(){var b=this,c=function(a){a.siblings(".selected").removeClass("selected"),a.addClass("selected");var c=a.data("id")===b.params.sidebar_id;b.container.find(".move-widget-btn").prop("disabled",c)};b.container.find(".widget-title-action").after(a(e.tpl.widget_reorder_nav));var f=a(_.template(e.tpl.move_widget_area,{sidebars:_(e.registered_sidebars.toArray()).pluck("attributes")}));b.container.find(".widget-top").after(f);var g=function(){var d=f.find("li"),g=d.filter(function(){return a(this).data("id")===b.params.sidebar_id});d.each(function(){var b=a(this),d=b.data("id"),f=e.registered_sidebars.get(d);b.toggle(f.get("is_rendered")),b.hasClass("selected")&&!f.get("is_rendered")&&c(g)})};g(),e.registered_sidebars.on("change:is_rendered",g);var h=b.container.find(".widget-reorder-nav");h.find(".move-widget, .move-widget-down, .move-widget-up").on("click keypress",function(c){if("keypress"!==c.type||13===c.which||32===c.which)if(a(this).focus(),a(this).is(".move-widget"))b.toggleWidgetMoveArea();else{var d=a(this).is(".move-widget-down"),e=a(this).is(".move-widget-up"),f=b.getWidgetSidebarPosition();if(e&&0===f||d&&f===b.getSidebarWidgetsControl().setting().length-1)return;e?b.moveUp():b.moveDown(),a(this).focus()}}),b.container.find(".widget-area-select").on("click keypress","li",function(b){("keypress"!==event.type||13===event.which||32===event.which)&&(b.preventDefault(),c(a(this)))}),b.container.find(".move-widget-btn").click(function(){b.getSidebarWidgetsControl().toggleReordering(!1);var a=b.params.sidebar_id,c=b.container.find(".widget-area-select li.selected").data("id"),e=d("sidebars_widgets["+a+"]"),f=d("sidebars_widgets["+c+"]"),g=Array.prototype.slice.call(e()),h=Array.prototype.slice.call(f()),i=b.getWidgetSidebarPosition();g.splice(i,1),h.push(b.params.widget_id),e(g),f(h),b.focus()})},_setupHighlightEffects:function(){var a=this;a.container.on("mouseenter click",function(){a.highlightPreviewWidget()}),a.setting.bind(function(){a.scrollPreviewWidgetIntoView(),a.highlightPreviewWidget()}),a.container.on("expand",function(){a.scrollPreviewWidgetIntoView()})},_setupUpdateUI:function(){var a=this,b=a.container.find(".widget-content"),c=a.container.find(".widget-control-save");c.val(e.i18n.save_btn_label),c.attr("title",e.i18n.save_btn_tooltip),c.removeClass("button-primary").addClass("button-secondary"),c.on("click",function(b){b.preventDefault(),a.updateWidget()});var d=_.debounce(function(){a.updateWidget()},250);a.container.find(".widget-content").on("keydown","input",function(b){13===b.which&&(b.preventDefault(),a.updateWidget({ignore_active_element:!0}))}),b.on("change input propertychange",":input",function(a){("change"===a.type||this.checkValidity&&this.checkValidity())&&d()}),a.setting.previewer.channel.bind("synced",function(){a.container.removeClass("previewer-loading")}),e.previewer.bind("widget-updated",function(b){b===a.params.widget_id&&a.container.removeClass("previewer-loading")}),e.previewer.bind("rendered-widgets",function(b){var c=!!b[a.params.widget_id];a.container.toggleClass("widget-rendered",c)})},_setupRemoveUI:function(){var a=this,b=a.container.find("a.widget-control-remove");b.on("click",function(b){b.preventDefault();var c;c=a.container.next().is(".customize-control-widget_form")?a.container.next().find(".widget-action:first"):a.container.prev().is(".customize-control-widget_form")?a.container.prev().find(".widget-action:first"):a.container.next(".customize-control-sidebar_widgets").find(".add-new-widget:first"),a.container.slideUp(function(){var b=e.getSidebarWidgetControlContainingWidget(a.params.widget_id);if(!b)throw new Error("Unable to find sidebars_widgets_control");var d=b.setting().slice(),f=d.indexOf(a.params.widget_id);if(-1===f)throw new Error("Widget is not in sidebar");d.splice(f,1),b.setting(d),c.focus()})});var c=function(){b.text(e.i18n.remove_btn_label),b.attr("title",e.i18n.remove_btn_tooltip)};a.params.is_new?wp.customize.bind("saved",c):c()},_getInputsSignature:function(b){var c=_(b).map(function(b){b=a(b);var c;return c=b.is("option")?[b.prop("nodeName"),b.prop("value")]:b.is(":checkbox, :radio")?[b.prop("type"),b.attr("id"),b.attr("name"),b.prop("value")]:[b.prop("nodeName"),b.attr("id"),b.attr("name"),b.attr("type")],c.join(",")});return c.join(";")},_getInputStatePropertyName:function(b){return b=a(b),b.is(":radio, :checkbox")?"checked":b.is("option")?"selected":"value"},getSidebarWidgetsControl:function(){var a=this,b="sidebars_widgets["+a.params.sidebar_id+"]",c=d.control(b);if(!c)throw new Error("Unable to locate sidebar_widgets control for "+a.params.sidebar_id);return c},updateWidget:function(b){var c=this;b=a.extend({instance:null,complete:null,ignore_active_element:!1},b);var d=b.instance,f=b.complete;c._update_count+=1;var g=c._update_count,h=c.container.find(".widget-content"),i=null,j=null,k=null;if(a.contains(c.container[0],document.activeElement)&&a(document.activeElement).is("[id]")){i=a(document.activeElement).prop("id");try{j=document.activeElement.selectionStart,k=document.activeElement.selectionEnd}catch(l){}}c.container.addClass("widget-form-loading"),c.container.addClass("previewer-loading");var m={};m.action=e.update_widget_ajax_action,m[e.update_widget_nonce_post_key]=e.update_widget_nonce_value;var n=a.param(m),o=h.find(":input, option");o.each(function(){var b=a(this),d=c._getInputStatePropertyName(this);b.data("state"+g,b.prop(d))}),n+=d?"&"+a.param({sanitized_widget_setting:JSON.stringify(d)}):"&"+o.serialize(),n+="&"+h.find("~ :input").serialize(),console.log(wp.ajax.settings.url,n);var p=a.post(wp.ajax.settings.url,n,function(d){if(d.success){var e=a(""+d.data.form+"
");c.hook("formUpdate",e);var l=e.find(":input, option"),m=c._getInputsSignature(o)===c._getInputsSignature(l);m?(o.each(function(d){var e=a(this),f=a(l[d]),h=c._getInputStatePropertyName(this),i=e.data("state"+g),j=f.prop(h);e.data("sanitized",j),i!==j?((b.ignore_active_element||!e.is(document.activeElement))&&e.prop(h,j),c.hook("unsanitaryField",e,j,i)):c.hook("sanitaryField",e,i)}),c.hook("formUpdated",e)):(h.html(e.html()),i&&a(document.getElementById(i)).prop({selectionStart:j,selectionEnd:k}).focus(),c.hook("formRefreshed"));var n=_(c.setting()).isEqual(d.data.instance);n?c.container.removeClass("previewer-loading"):(c.is_widget_updating=!0,c.setting(d.data.instance),c.is_widget_updating=!1),f&&f.call(c,null,{no_change:n,ajax_finished:!0})}else{console.log(d);var p="FAIL";if(d.data&&d.data.message&&(p=d.data.message),!f)throw new Error(p);f.call(c,p)}});p.fail(function(a,b){if(!f)throw new Error(b);f.call(c,b)}),p.always(function(){c.container.removeClass("widget-form-loading"),o.each(function(){a(this).removeData("state"+g)})})},expandControlSection:function(){var a=this.container.closest(".accordion-section");a.hasClass("open")||a.find(".accordion-section-title:first").trigger("click")},expandForm:function(){this.toggleForm(!0)},collapseForm:function(){this.toggleForm(!1)},toggleForm:function(a){var b=this,c=b.container.find("div.widget:first"),d=c.find(".widget-inside:first");if("undefined"==typeof a&&(a=!d.is(":visible")),d.is(":visible")!==a){var e;a?(wp.customize.control.each(function(a){b.params.type===a.params.type&&b!==a&&a.collapseForm()}),b.container.trigger("expand"),b.container.addClass("expanding"),e=function(){b.container.removeClass("expanding"),b.container.addClass("expanded"),b.container.trigger("expanded")},b.params.is_wide?d.animate({width:"show"},"fast",e):d.slideDown("fast",e)):(b.container.trigger("collapse"),b.container.addClass("collapsing"),e=function(){b.container.removeClass("collapsing"),b.container.removeClass("expanded"),b.container.trigger("collapsed")},b.params.is_wide?d.animate({width:"hide"},"fast",e):d.slideUp("fast",function(){c.css({width:"",margin:""}),e()}))}},focus:function(){var a=this;a.expandControlSection(),a.expandForm(),a.container.find(":focusable:first").focus().trigger("click")},getWidgetSidebarPosition:function(){var a=this,b=a.getSidebarWidgetsControl().setting(),c=b.indexOf(a.params.widget_id);if(-1===c)throw new Error("Widget was unexpectedly not present in the sidebar.");return c},moveUp:function(){this._moveWidgetByOne(-1)},moveDown:function(){this._moveWidgetByOne(1)},_moveWidgetByOne:function(a){var b=this,c=b.getWidgetSidebarPosition(),d=b.getSidebarWidgetsControl().setting,e=Array.prototype.slice.call(d()),f=e[c+a];e[c+a]=b.params.widget_id,e[c]=f,d(e)},toggleWidgetMoveArea:function(b){var c=this,d=c.container.find(".move-widget-area");"undefined"==typeof b&&(b=!d.hasClass("active")),b&&(d.find(".selected").removeClass("selected"),d.find("li").filter(function(){return a(this).data("id")===c.params.sidebar_id}).addClass("selected"),c.container.find(".move-widget-btn").prop("disabled",!0)),d.toggleClass("active",b)},getPreviewWidgetElement:function(){var a=this,b=e.getPreviewWindow().WidgetCustomizerPreview;return b.getSidebarWidgetElement(a.params.sidebar_id,a.params.widget_id)},scrollPreviewWidgetIntoView:function(){},highlightSectionAndControl:function(){var b,c=this;b=c.container.is(":hidden")?c.container.closest(".control-section"):c.container,a(".widget-customizer-highlighted").removeClass("widget-customizer-highlighted"),b.addClass("widget-customizer-highlighted"),setTimeout(function(){b.removeClass("widget-customizer-highlighted")},500)},highlightPreviewWidget:function(){var a=this,b=a.getPreviewWidgetElement(),c=b.closest("html");c.find(".widget-customizer-highlighted-widget").removeClass("widget-customizer-highlighted-widget"),b.addClass("widget-customizer-highlighted-widget"),setTimeout(function(){b.removeClass("widget-customizer-highlighted-widget")},500)}});var j=wp.customize.Previewer;return wp.customize.Previewer=j.extend({initialize:function(a,b){e.previewer=this,j.prototype.initialize.call(this,a,b),this.bind("refresh",this.refresh)}}),e.getSidebarWidgetControlContainingWidget=function(a){var b=null;return wp.customize.control.each(function(c){"sidebar_widgets"===c.params.type&&-1!==c.setting().indexOf(a)&&(b=c)}),b},e.getWidgetFormControlForWidget=function(a){var b=null;return wp.customize.control.each(function(c){"widget_form"===c.params.type&&c.params.widget_id===a&&(b=c)}),b},e.getPreviewWindow=function(){return a("#customize-preview").find("iframe").prop("contentWindow")},e.availableWidgetsPanel={active_sidebar_widgets_control:null,selected_widget_tpl:null,container:null,filter_input:null,setup:function(){var b=this;b.container=a("#available-widgets"),b.filter_input=a("#available-widgets-filter").find("input");var c=function(){e.available_widgets.each(function(c){var d=a("#widget-tpl-"+c.id);d.toggle(!c.get("is_disabled")),c.get("is_disabled")&&d.is(b.selected_widget_tpl)&&(b.selected_widget_tpl=null)})};e.available_widgets.on("change",c),c(),a("#customize-controls").on("click keydown",function(c){var d=a(c.target).is(".add-new-widget, .add-new-widget *");a("body").hasClass("adding-widget")&&!d&&b.close()}),e.previewer.bind("url",function(){b.close()}),b.container.find(".widget-tpl").on("click keypress",function(a){("keypress"!==a.type||13===a.which||32===a.which)&&b.submit(this)}),b.container.liveFilter("#available-widgets-filter input",".widget-tpl",{filterChildSelector:".widget-title h4",after:function(){var a=b.filter_input.val();if(b.selected_widget_tpl&&!b.selected_widget_tpl.is(":visible")&&(b.selected_widget_tpl.removeClass("selected"),b.selected_widget_tpl=null),b.selected_widget_tpl&&!a&&(b.selected_widget_tpl.removeClass("selected"),b.selected_widget_tpl=null),!b.selected_widget_tpl&&a){var c=b.container.find("> .widget-tpl:visible:first");c.length&&b.select(c)}}}),b.container.find(" > .widget-tpl").on("focus",function(){b.select(this)}),b.container.on("keydown",function(c){var d=13===c.which,e=27===c.which,f=40===c.which,g=38===c.which,h=null,i=b.container.find("> .widget-tpl:visible:first"),j=b.container.find("> .widget-tpl:visible:last"),k=a(c.target).is(b.filter_input);return f||g?(f?k?h=i:b.selected_widget_tpl&&0!==b.selected_widget_tpl.nextAll(".widget-tpl:visible").length&&(h=b.selected_widget_tpl.nextAll(".widget-tpl:visible:first")):g&&(k?h=j:b.selected_widget_tpl&&0!==b.selected_widget_tpl.prevAll(".widget-tpl:visible").length&&(h=b.selected_widget_tpl.prevAll(".widget-tpl:visible:first"))),b.select(h),void(h?h.focus():b.filter_input.focus())):void((!d||b.filter_input.val())&&(d?b.submit():e&&b.close({return_focus:!0})))})},select:function(b){var c=this;c.selected_widget_tpl=a(b),c.selected_widget_tpl.siblings(".widget-tpl").removeClass("selected"),c.selected_widget_tpl.addClass("selected")},submit:function(b){var c=this;if(b||(b=c.selected_widget_tpl),b&&c.active_sidebar_widgets_control){c.select(b);var d=a(c.selected_widget_tpl).data("widget-id"),f=e.available_widgets.findWhere({id:d});if(!f)throw new Error("Widget unexpectedly not found.");c.active_sidebar_widgets_control.addWidget(f.get("id_base")),c.close()}},open:function(b){var c=this;c.active_sidebar_widgets_control=b,_(b.getWidgetFormControls()).each(function(a){a.params.is_wide&&a.collapseForm()}),a("body").addClass("adding-widget"),c.container.find(".widget-tpl").removeClass("selected"),c.filter_input.focus()},close:function(b){var c=this;b=b||{},b.return_focus&&c.active_sidebar_widgets_control&&c.active_sidebar_widgets_control.container.find(".add-new-widget").focus(),c.active_sidebar_widgets_control=null,c.selected_widget_tpl=null,a("body").removeClass("adding-widget"),c.filter_input.val("")}},e}(jQuery);!function(a){a.fn.liveFilter=function(b,c,d){var e={filterChildSelector:null,filter:function(b,c){return a(b).text().toUpperCase().indexOf(c.toUpperCase())>=0},before:function(){},after:function(){}};d=a.extend(e,d);var f=a(this).find(c);d.filterChildSelector&&(f=f.find(d.filterChildSelector));var g=d.filter;a(b).keyup(function(){var b=a(this).val(),e=f.filter(function(){return g(this,b)}),h=f.not(e);d.filterChildSelector&&(e=e.parents(c),h=h.parents(c).hide()),d.before.call(this,e,h),e.show(),h.hide(),""===b&&(e.show(),h.show()),d.after.call(this,e,h)})}}(jQuery);
\ No newline at end of file
diff --git a/wp-includes/class-wp-customize-control.php b/wp-includes/class-wp-customize-control.php
index d714c03c4d..6dc205d781 100644
--- a/wp-includes/class-wp-customize-control.php
+++ b/wp-includes/class-wp-customize-control.php
@@ -813,4 +813,76 @@ class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control {
foreach ( $this->default_headers as $choice => $header )
$this->print_header_image( $choice, $header );
}
-}
\ No newline at end of file
+}
+
+/**
+ * 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() {
+ ?>
+
+
+
+
+
+
+
+
+ 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 );
+ }
+}
+
diff --git a/wp-includes/class-wp-customize-manager.php b/wp-includes/class-wp-customize-manager.php
index d68ff91cfd..802d30a5aa 100644
--- a/wp-includes/class-wp-customize-manager.php
+++ b/wp-includes/class-wp-customize-manager.php
@@ -61,6 +61,9 @@ final class WP_Customize_Manager {
require( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
require( ABSPATH . WPINC . '/class-wp-customize-section.php' );
require( ABSPATH . WPINC . '/class-wp-customize-control.php' );
+ require( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
+
+ WP_Customize_Widgets::setup(); // This should be integrated.
add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
diff --git a/wp-includes/class-wp-customize-widgets.php b/wp-includes/class-wp-customize-widgets.php
new file mode 100644
index 0000000000..8b0c6f6556
--- /dev/null
+++ b/wp-includes/class-wp-customize-widgets.php
@@ -0,0 +1,1264 @@
+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(
+ '%2$s%4$s%6$s
',
+ 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' ),
+ ),
+ '
+
+ '
+ );
+
+ // 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() {
+ ?>
+
+ '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(
+ '