57 */ |
57 */ |
58 api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{ |
58 api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{ |
59 model: api.Widgets.WidgetModel, |
59 model: api.Widgets.WidgetModel, |
60 |
60 |
61 // Controls searching on the current widget collection |
61 // Controls searching on the current widget collection |
62 // and triggers an update event |
62 // and triggers an update event. |
63 doSearch: function( value ) { |
63 doSearch: function( value ) { |
64 |
64 |
65 // Don't do anything if we've already done this search |
65 // Don't do anything if we've already done this search. |
66 // Useful because the search handler fires multiple times per keystroke |
66 // Useful because the search handler fires multiple times per keystroke. |
67 if ( this.terms === value ) { |
67 if ( this.terms === value ) { |
68 return; |
68 return; |
69 } |
69 } |
70 |
70 |
71 // Updates terms with the value passed |
71 // Updates terms with the value passed. |
72 this.terms = value; |
72 this.terms = value; |
73 |
73 |
74 // If we have terms, run a search... |
74 // If we have terms, run a search... |
75 if ( this.terms.length > 0 ) { |
75 if ( this.terms.length > 0 ) { |
76 this.search( this.terms ); |
76 this.search( this.terms ); |
82 widget.set( 'search_matched', true ); |
82 widget.set( 'search_matched', true ); |
83 } ); |
83 } ); |
84 } |
84 } |
85 }, |
85 }, |
86 |
86 |
87 // Performs a search within the collection |
87 // Performs a search within the collection. |
88 // @uses RegExp |
88 // @uses RegExp |
89 search: function( term ) { |
89 search: function( term ) { |
90 var match, haystack; |
90 var match, haystack; |
91 |
91 |
92 // Escape the term string for RegExp meta characters |
92 // Escape the term string for RegExp meta characters. |
93 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); |
93 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); |
94 |
94 |
95 // Consider spaces as word delimiters and match the whole string |
95 // Consider spaces as word delimiters and match the whole string |
96 // so matching terms can be combined |
96 // so matching terms can be combined. |
97 term = term.replace( / /g, ')(?=.*' ); |
97 term = term.replace( / /g, ')(?=.*' ); |
98 match = new RegExp( '^(?=.*' + term + ').+', 'i' ); |
98 match = new RegExp( '^(?=.*' + term + ').+', 'i' ); |
99 |
99 |
100 this.each( function ( data ) { |
100 this.each( function ( data ) { |
101 haystack = [ data.get( 'name' ), data.get( 'id' ), data.get( 'description' ) ].join( ' ' ); |
101 haystack = [ data.get( 'name' ), data.get( 'id' ), data.get( 'description' ) ].join( ' ' ); |
148 'click .widget-tpl' : '_submit', |
148 'click .widget-tpl' : '_submit', |
149 'keypress .widget-tpl' : '_submit', |
149 'keypress .widget-tpl' : '_submit', |
150 'keydown' : 'keyboardAccessible' |
150 'keydown' : 'keyboardAccessible' |
151 }, |
151 }, |
152 |
152 |
153 // Cache current selected widget |
153 // Cache current selected widget. |
154 selected: null, |
154 selected: null, |
155 |
155 |
156 // Cache sidebar control which has opened panel |
156 // Cache sidebar control which has opened panel. |
157 currentSidebarControl: null, |
157 currentSidebarControl: null, |
158 $search: null, |
158 $search: null, |
159 $clearResults: null, |
159 $clearResults: null, |
160 searchMatchesCount: null, |
160 searchMatchesCount: null, |
161 |
161 |
179 this.updateList(); |
179 this.updateList(); |
180 |
180 |
181 // Set the initial search count to the number of available widgets. |
181 // Set the initial search count to the number of available widgets. |
182 this.searchMatchesCount = this.collection.length; |
182 this.searchMatchesCount = this.collection.length; |
183 |
183 |
184 // If the available widgets panel is open and the customize controls are |
184 /* |
185 // interacted with (i.e. available widgets panel is blurred) then close the |
185 * If the available widgets panel is open and the customize controls |
186 // available widgets panel. Also close on back button click. |
186 * are interacted with (i.e. available widgets panel is blurred) then |
|
187 * close the available widgets panel. Also close on back button click. |
|
188 */ |
187 $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) { |
189 $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) { |
188 var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' ); |
190 var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' ); |
189 if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) { |
191 if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) { |
190 self.close(); |
192 self.close(); |
191 } |
193 } |
192 } ); |
194 } ); |
193 |
195 |
194 // Clear the search results and trigger a `keyup` event to fire a new search. |
196 // Clear the search results and trigger an `input` event to fire a new search. |
195 this.$clearResults.on( 'click', function() { |
197 this.$clearResults.on( 'click', function() { |
196 self.$search.val( '' ).focus().trigger( 'keyup' ); |
198 self.$search.val( '' ).focus().trigger( 'input' ); |
197 } ); |
199 } ); |
198 |
200 |
199 // Close the panel if the URL in the preview changes |
201 // Close the panel if the URL in the preview changes. |
200 api.previewer.bind( 'url', this.close ); |
202 api.previewer.bind( 'url', this.close ); |
201 }, |
203 }, |
202 |
204 |
203 /** |
205 /** |
204 * Performs a search and handles selected widget. |
206 * Performs a search and handles selected widget. |
210 // Update the search matches count. |
212 // Update the search matches count. |
211 this.updateSearchMatchesCount(); |
213 this.updateSearchMatchesCount(); |
212 // Announce how many search results. |
214 // Announce how many search results. |
213 this.announceSearchMatches(); |
215 this.announceSearchMatches(); |
214 |
216 |
215 // Remove a widget from being selected if it is no longer visible |
217 // Remove a widget from being selected if it is no longer visible. |
216 if ( this.selected && ! this.selected.is( ':visible' ) ) { |
218 if ( this.selected && ! this.selected.is( ':visible' ) ) { |
217 this.selected.removeClass( 'selected' ); |
219 this.selected.removeClass( 'selected' ); |
218 this.selected = null; |
220 this.selected = null; |
219 } |
221 } |
220 |
222 |
221 // If a widget was selected but the filter value has been cleared out, clear selection |
223 // If a widget was selected but the filter value has been cleared out, clear selection. |
222 if ( this.selected && ! event.target.value ) { |
224 if ( this.selected && ! event.target.value ) { |
223 this.selected.removeClass( 'selected' ); |
225 this.selected.removeClass( 'selected' ); |
224 this.selected = null; |
226 this.selected = null; |
225 } |
227 } |
226 |
228 |
227 // If a filter has been entered and a widget hasn't been selected, select the first one shown |
229 // If a filter has been entered and a widget hasn't been selected, select the first one shown. |
228 if ( ! this.selected && event.target.value ) { |
230 if ( ! this.selected && event.target.value ) { |
229 firstVisible = this.$el.find( '> .widget-tpl:visible:first' ); |
231 firstVisible = this.$el.find( '> .widget-tpl:visible:first' ); |
230 if ( firstVisible.length ) { |
232 if ( firstVisible.length ) { |
231 this.select( firstVisible ); |
233 this.select( firstVisible ); |
232 } |
234 } |
455 api.Widgets.formSyncHandlers = { |
457 api.Widgets.formSyncHandlers = { |
456 |
458 |
457 /** |
459 /** |
458 * @param {jQuery.Event} e |
460 * @param {jQuery.Event} e |
459 * @param {jQuery} widget |
461 * @param {jQuery} widget |
460 * @param {String} newForm |
462 * @param {string} newForm |
461 */ |
463 */ |
462 rss: function( e, widget, newForm ) { |
464 rss: function( e, widget, newForm ) { |
463 var oldWidgetError = widget.find( '.widget-error:first' ), |
465 var oldWidgetError = widget.find( '.widget-error:first' ), |
464 newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' ); |
466 newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' ); |
465 |
467 |
599 * Handle changes to the setting |
601 * Handle changes to the setting |
600 */ |
602 */ |
601 _setupModel: function() { |
603 _setupModel: function() { |
602 var self = this, rememberSavedWidgetId; |
604 var self = this, rememberSavedWidgetId; |
603 |
605 |
604 // Remember saved widgets so we know which to trash (move to inactive widgets sidebar) |
606 // Remember saved widgets so we know which to trash (move to inactive widgets sidebar). |
605 rememberSavedWidgetId = function() { |
607 rememberSavedWidgetId = function() { |
606 api.Widgets.savedWidgetIds[self.params.widget_id] = true; |
608 api.Widgets.savedWidgetIds[self.params.widget_id] = true; |
607 }; |
609 }; |
608 api.bind( 'ready', rememberSavedWidgetId ); |
610 api.bind( 'ready', rememberSavedWidgetId ); |
609 api.bind( 'saved', rememberSavedWidgetId ); |
611 api.bind( 'saved', rememberSavedWidgetId ); |
610 |
612 |
611 this._updateCount = 0; |
613 this._updateCount = 0; |
612 this.isWidgetUpdating = false; |
614 this.isWidgetUpdating = false; |
613 this.liveUpdateMode = true; |
615 this.liveUpdateMode = true; |
614 |
616 |
615 // Update widget whenever model changes |
617 // Update widget whenever model changes. |
616 this.setting.bind( function( to, from ) { |
618 this.setting.bind( function( to, from ) { |
617 if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) { |
619 if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) { |
618 self.updateWidget( { instance: to } ); |
620 self.updateWidget( { instance: to } ); |
619 } |
621 } |
620 } ); |
622 } ); |
655 windowHeight = $( window ).height(), |
657 windowHeight = $( window ).height(), |
656 formHeight = $widgetForm.outerHeight(), |
658 formHeight = $widgetForm.outerHeight(), |
657 top; |
659 top; |
658 $widgetInside.css( 'max-height', windowHeight ); |
660 $widgetInside.css( 'max-height', windowHeight ); |
659 top = Math.max( |
661 top = Math.max( |
660 0, // prevent top from going off screen |
662 0, // Prevent top from going off screen. |
661 Math.min( |
663 Math.min( |
662 Math.max( offsetTop, 0 ), // distance widget in panel is from top of screen |
664 Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen. |
663 windowHeight - formHeight // flush up against bottom of screen |
665 windowHeight - formHeight // Flush up against bottom of screen. |
664 ) |
666 ) |
665 ); |
667 ); |
666 $widgetInside.css( 'top', top ); |
668 $widgetInside.css( 'top', top ); |
667 }; |
669 }; |
668 |
670 |
677 $customizeSidebar.off( 'scroll', positionWidget ); |
679 $customizeSidebar.off( 'scroll', positionWidget ); |
678 $( window ).off( 'resize', positionWidget ); |
680 $( window ).off( 'resize', positionWidget ); |
679 $themeControlsContainer.off( 'expanded collapsed', positionWidget ); |
681 $themeControlsContainer.off( 'expanded collapsed', positionWidget ); |
680 } ); |
682 } ); |
681 |
683 |
682 // Reposition whenever a sidebar's widgets are changed |
684 // Reposition whenever a sidebar's widgets are changed. |
683 api.each( function( setting ) { |
685 api.each( function( setting ) { |
684 if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) { |
686 if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) { |
685 setting.bind( function() { |
687 setting.bind( function() { |
686 if ( self.container.hasClass( 'expanded' ) ) { |
688 if ( self.container.hasClass( 'expanded' ) ) { |
687 positionWidget(); |
689 positionWidget(); |
883 * Highlight widgets in preview when interacted with in the Customizer |
885 * Highlight widgets in preview when interacted with in the Customizer |
884 */ |
886 */ |
885 _setupHighlightEffects: function() { |
887 _setupHighlightEffects: function() { |
886 var self = this; |
888 var self = this; |
887 |
889 |
888 // Highlight whenever hovering or clicking over the form |
890 // Highlight whenever hovering or clicking over the form. |
889 this.container.on( 'mouseenter click', function() { |
891 this.container.on( 'mouseenter click', function() { |
890 self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); |
892 self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); |
891 } ); |
893 } ); |
892 |
894 |
893 // Highlight when the setting is updated |
895 // Highlight when the setting is updated. |
894 this.setting.bind( function() { |
896 this.setting.bind( function() { |
895 self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); |
897 self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); |
896 } ); |
898 } ); |
897 }, |
899 }, |
898 |
900 |
904 $saveBtn, updateWidgetDebounced, formSyncHandler; |
906 $saveBtn, updateWidgetDebounced, formSyncHandler; |
905 |
907 |
906 $widgetRoot = this.container.find( '.widget:first' ); |
908 $widgetRoot = this.container.find( '.widget:first' ); |
907 $widgetContent = $widgetRoot.find( '.widget-content:first' ); |
909 $widgetContent = $widgetRoot.find( '.widget-content:first' ); |
908 |
910 |
909 // Configure update button |
911 // Configure update button. |
910 $saveBtn = this.container.find( '.widget-control-save' ); |
912 $saveBtn = this.container.find( '.widget-control-save' ); |
911 $saveBtn.val( l10n.saveBtnLabel ); |
913 $saveBtn.val( l10n.saveBtnLabel ); |
912 $saveBtn.attr( 'title', l10n.saveBtnTooltip ); |
914 $saveBtn.attr( 'title', l10n.saveBtnTooltip ); |
913 $saveBtn.removeClass( 'button-primary' ); |
915 $saveBtn.removeClass( 'button-primary' ); |
914 $saveBtn.on( 'click', function( e ) { |
916 $saveBtn.on( 'click', function( e ) { |
918 |
920 |
919 updateWidgetDebounced = _.debounce( function() { |
921 updateWidgetDebounced = _.debounce( function() { |
920 self.updateWidget(); |
922 self.updateWidget(); |
921 }, 250 ); |
923 }, 250 ); |
922 |
924 |
923 // Trigger widget form update when hitting Enter within an input |
925 // Trigger widget form update when hitting Enter within an input. |
924 $widgetContent.on( 'keydown', 'input', function( e ) { |
926 $widgetContent.on( 'keydown', 'input', function( e ) { |
925 if ( 13 === e.which ) { // Enter |
927 if ( 13 === e.which ) { // Enter. |
926 e.preventDefault(); |
928 e.preventDefault(); |
927 self.updateWidget( { ignoreActiveElement: true } ); |
929 self.updateWidget( { ignoreActiveElement: true } ); |
928 } |
930 } |
929 } ); |
931 } ); |
930 |
932 |
931 // Handle widgets that support live previews |
933 // Handle widgets that support live previews. |
932 $widgetContent.on( 'change input propertychange', ':input', function( e ) { |
934 $widgetContent.on( 'change input propertychange', ':input', function( e ) { |
933 if ( ! self.liveUpdateMode ) { |
935 if ( ! self.liveUpdateMode ) { |
934 return; |
936 return; |
935 } |
937 } |
936 if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) { |
938 if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) { |
937 updateWidgetDebounced(); |
939 updateWidgetDebounced(); |
938 } |
940 } |
939 } ); |
941 } ); |
940 |
942 |
941 // Remove loading indicators when the setting is saved and the preview updates |
943 // Remove loading indicators when the setting is saved and the preview updates. |
942 this.setting.previewer.channel.bind( 'synced', function() { |
944 this.setting.previewer.channel.bind( 'synced', function() { |
943 self.container.removeClass( 'previewer-loading' ); |
945 self.container.removeClass( 'previewer-loading' ); |
944 } ); |
946 } ); |
945 |
947 |
946 api.previewer.bind( 'widget-updated', function( updatedWidgetId ) { |
948 api.previewer.bind( 'widget-updated', function( updatedWidgetId ) { |
964 * |
966 * |
965 * Overrides api.Control.toggle() |
967 * Overrides api.Control.toggle() |
966 * |
968 * |
967 * @since 4.1.0 |
969 * @since 4.1.0 |
968 * |
970 * |
969 * @param {Boolean} active |
971 * @param {boolean} active |
970 * @param {Object} args |
972 * @param {Object} args |
971 * @param {function} args.completeCallback |
973 * @param {function} args.completeCallback |
972 */ |
974 */ |
973 onChangeActive: function ( active, args ) { |
975 onChangeActive: function ( active, args ) { |
974 // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments |
976 // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments. |
975 this.container.toggleClass( 'widget-rendered', active ); |
977 this.container.toggleClass( 'widget-rendered', active ); |
976 if ( args.completeCallback ) { |
978 if ( args.completeCallback ) { |
977 args.completeCallback(); |
979 args.completeCallback(); |
978 } |
980 } |
979 }, |
981 }, |
982 * Set up event handlers for widget removal |
984 * Set up event handlers for widget removal |
983 */ |
985 */ |
984 _setupRemoveUI: function() { |
986 _setupRemoveUI: function() { |
985 var self = this, $removeBtn, replaceDeleteWithRemove; |
987 var self = this, $removeBtn, replaceDeleteWithRemove; |
986 |
988 |
987 // Configure remove button |
989 // Configure remove button. |
988 $removeBtn = this.container.find( '.widget-control-remove' ); |
990 $removeBtn = this.container.find( '.widget-control-remove' ); |
989 $removeBtn.on( 'click', function() { |
991 $removeBtn.on( 'click', function() { |
990 // Find an adjacent element to add focus to when this widget goes away |
992 // Find an adjacent element to add focus to when this widget goes away. |
991 var $adjacentFocusTarget; |
993 var $adjacentFocusTarget; |
992 if ( self.container.next().is( '.customize-control-widget_form' ) ) { |
994 if ( self.container.next().is( '.customize-control-widget_form' ) ) { |
993 $adjacentFocusTarget = self.container.next().find( '.widget-action:first' ); |
995 $adjacentFocusTarget = self.container.next().find( '.widget-action:first' ); |
994 } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) { |
996 } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) { |
995 $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' ); |
997 $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' ); |
1012 } |
1014 } |
1013 |
1015 |
1014 sidebarWidgetIds.splice( i, 1 ); |
1016 sidebarWidgetIds.splice( i, 1 ); |
1015 sidebarsWidgetsControl.setting( sidebarWidgetIds ); |
1017 sidebarsWidgetsControl.setting( sidebarWidgetIds ); |
1016 |
1018 |
1017 $adjacentFocusTarget.focus(); // keyboard accessibility |
1019 $adjacentFocusTarget.focus(); // Keyboard accessibility. |
1018 } ); |
1020 } ); |
1019 } ); |
1021 } ); |
1020 |
1022 |
1021 replaceDeleteWithRemove = function() { |
1023 replaceDeleteWithRemove = function() { |
1022 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete" |
1024 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete". |
1023 $removeBtn.attr( 'title', l10n.removeBtnTooltip ); |
1025 $removeBtn.attr( 'title', l10n.removeBtnTooltip ); |
1024 }; |
1026 }; |
1025 |
1027 |
1026 if ( this.params.is_new ) { |
1028 if ( this.params.is_new ) { |
1027 api.bind( 'saved', replaceDeleteWithRemove ); |
1029 api.bind( 'saved', replaceDeleteWithRemove ); |
1036 * be aligned to copy the sanitized over. The elements returned by this |
1038 * be aligned to copy the sanitized over. The elements returned by this |
1037 * are passed into this._getInputsSignature(), and they are iterated |
1039 * are passed into this._getInputsSignature(), and they are iterated |
1038 * over when copying sanitized values over to the form loaded. |
1040 * over when copying sanitized values over to the form loaded. |
1039 * |
1041 * |
1040 * @param {jQuery} container element in which to look for inputs |
1042 * @param {jQuery} container element in which to look for inputs |
1041 * @returns {jQuery} inputs |
1043 * @return {jQuery} inputs |
1042 * @private |
1044 * @private |
1043 */ |
1045 */ |
1044 _getInputs: function( container ) { |
1046 _getInputs: function( container ) { |
1045 return $( container ).find( ':input[name]' ); |
1047 return $( container ).find( ':input[name]' ); |
1046 }, |
1048 }, |
1091 |
1093 |
1092 /** |
1094 /** |
1093 * Update an input's state based on its type. |
1095 * Update an input's state based on its type. |
1094 * |
1096 * |
1095 * @param {jQuery|Element} input |
1097 * @param {jQuery|Element} input |
1096 * @param {string|boolean|array|*} state |
1098 * @param {string|boolean|Array|*} state |
1097 * @private |
1099 * @private |
1098 */ |
1100 */ |
1099 _setInputState: function ( input, state ) { |
1101 _setInputState: function ( input, state ) { |
1100 input = $( input ); |
1102 input = $( input ); |
1101 if ( input.is( ':radio, :checkbox' ) ) { |
1103 if ( input.is( ':radio, :checkbox' ) ) { |
1102 input.prop( 'checked', state ); |
1104 input.prop( 'checked', state ); |
1103 } else if ( input.is( 'select[multiple]' ) ) { |
1105 } else if ( input.is( 'select[multiple]' ) ) { |
1104 if ( ! $.isArray( state ) ) { |
1106 if ( ! $.isArray( state ) ) { |
1105 state = []; |
1107 state = []; |
1106 } else { |
1108 } else { |
1107 // Make sure all state items are strings since the DOM value is a string |
1109 // Make sure all state items are strings since the DOM value is a string. |
1108 state = _.map( state, function ( value ) { |
1110 state = _.map( state, function ( value ) { |
1109 return String( value ); |
1111 return String( value ); |
1110 } ); |
1112 } ); |
1111 } |
1113 } |
1112 input.find( 'option' ).each( function () { |
1114 input.find( 'option' ).each( function () { |
1139 |
1141 |
1140 /** |
1142 /** |
1141 * Submit the widget form via Ajax and get back the updated instance, |
1143 * Submit the widget form via Ajax and get back the updated instance, |
1142 * along with the new widget control form to render. |
1144 * along with the new widget control form to render. |
1143 * |
1145 * |
1144 * @param {object} [args] |
1146 * @param {Object} [args] |
1145 * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used |
1147 * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used |
1146 * @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. |
1148 * @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. |
1147 * @param {Boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element. |
1149 * @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element. |
1148 */ |
1150 */ |
1149 updateWidget: function( args ) { |
1151 updateWidget: function( args ) { |
1150 var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent, |
1152 var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent, |
1151 updateNumber, params, data, $inputs, processing, jqxhr, isChanged; |
1153 updateNumber, params, data, $inputs, processing, jqxhr, isChanged; |
1152 |
1154 |
1166 updateNumber = this._updateCount; |
1168 updateNumber = this._updateCount; |
1167 |
1169 |
1168 $widgetRoot = this.container.find( '.widget:first' ); |
1170 $widgetRoot = this.container.find( '.widget:first' ); |
1169 $widgetContent = $widgetRoot.find( '.widget-content:first' ); |
1171 $widgetContent = $widgetRoot.find( '.widget-content:first' ); |
1170 |
1172 |
1171 // Remove a previous error message |
1173 // Remove a previous error message. |
1172 $widgetContent.find( '.widget-error' ).remove(); |
1174 $widgetContent.find( '.widget-error' ).remove(); |
1173 |
1175 |
1174 this.container.addClass( 'widget-form-loading' ); |
1176 this.container.addClass( 'widget-form-loading' ); |
1175 this.container.addClass( 'previewer-loading' ); |
1177 this.container.addClass( 'previewer-loading' ); |
1176 processing = api.state( 'processing' ); |
1178 processing = api.state( 'processing' ); |
1188 params.customized = wp.customize.previewer.query().customized; |
1190 params.customized = wp.customize.previewer.query().customized; |
1189 |
1191 |
1190 data = $.param( params ); |
1192 data = $.param( params ); |
1191 $inputs = this._getInputs( $widgetContent ); |
1193 $inputs = this._getInputs( $widgetContent ); |
1192 |
1194 |
1193 // Store the value we're submitting in data so that when the response comes back, |
1195 /* |
1194 // we know if it got sanitized; if there is no difference in the sanitized value, |
1196 * Store the value we're submitting in data so that when the response comes back, |
1195 // then we do not need to touch the UI and mess up the user's ongoing editing. |
1197 * we know if it got sanitized; if there is no difference in the sanitized value, |
|
1198 * then we do not need to touch the UI and mess up the user's ongoing editing. |
|
1199 */ |
1196 $inputs.each( function() { |
1200 $inputs.each( function() { |
1197 $( this ).data( 'state' + updateNumber, self._getInputState( this ) ); |
1201 $( this ).data( 'state' + updateNumber, self._getInputState( this ) ); |
1198 } ); |
1202 } ); |
1199 |
1203 |
1200 if ( instanceOverride ) { |
1204 if ( instanceOverride ) { |
1233 if ( r.success ) { |
1237 if ( r.success ) { |
1234 sanitizedForm = $( '<div>' + r.data.form + '</div>' ); |
1238 sanitizedForm = $( '<div>' + r.data.form + '</div>' ); |
1235 $sanitizedInputs = self._getInputs( sanitizedForm ); |
1239 $sanitizedInputs = self._getInputs( sanitizedForm ); |
1236 hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs ); |
1240 hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs ); |
1237 |
1241 |
1238 // Restore live update mode if sanitized fields are now aligned with the existing fields |
1242 // Restore live update mode if sanitized fields are now aligned with the existing fields. |
1239 if ( hasSameInputsInResponse && ! self.liveUpdateMode ) { |
1243 if ( hasSameInputsInResponse && ! self.liveUpdateMode ) { |
1240 self.liveUpdateMode = true; |
1244 self.liveUpdateMode = true; |
1241 self.container.removeClass( 'widget-form-disabled' ); |
1245 self.container.removeClass( 'widget-form-disabled' ); |
1242 self.container.find( 'input[name="savewidget"]' ).hide(); |
1246 self.container.find( 'input[name="savewidget"]' ).hide(); |
1243 } |
1247 } |
1244 |
1248 |
1245 // Sync sanitized field states to existing fields if they are aligned |
1249 // Sync sanitized field states to existing fields if they are aligned. |
1246 if ( hasSameInputsInResponse && self.liveUpdateMode ) { |
1250 if ( hasSameInputsInResponse && self.liveUpdateMode ) { |
1247 $inputs.each( function( i ) { |
1251 $inputs.each( function( i ) { |
1248 var $input = $( this ), |
1252 var $input = $( this ), |
1249 $sanitizedInput = $( $sanitizedInputs[i] ), |
1253 $sanitizedInput = $( $sanitizedInputs[i] ), |
1250 submittedState, sanitizedState, canUpdateState; |
1254 submittedState, sanitizedState, canUpdateState; |
1259 } |
1263 } |
1260 } ); |
1264 } ); |
1261 |
1265 |
1262 $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] ); |
1266 $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] ); |
1263 |
1267 |
1264 // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled |
1268 // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled. |
1265 } else if ( self.liveUpdateMode ) { |
1269 } else if ( self.liveUpdateMode ) { |
1266 self.liveUpdateMode = false; |
1270 self.liveUpdateMode = false; |
1267 self.container.find( 'input[name="savewidget"]' ).show(); |
1271 self.container.find( 'input[name="savewidget"]' ).show(); |
1268 isLiveUpdateAborted = true; |
1272 isLiveUpdateAborted = true; |
1269 |
1273 |
1270 // Otherwise, replace existing form with the sanitized form |
1274 // Otherwise, replace existing form with the sanitized form. |
1271 } else { |
1275 } else { |
1272 $widgetContent.html( r.data.form ); |
1276 $widgetContent.html( r.data.form ); |
1273 |
1277 |
1274 self.container.removeClass( 'widget-form-disabled' ); |
1278 self.container.removeClass( 'widget-form-disabled' ); |
1275 |
1279 |
1281 * needing to be rendered, and so we can preempt the event for the |
1285 * needing to be rendered, and so we can preempt the event for the |
1282 * preview finishing loading. |
1286 * preview finishing loading. |
1283 */ |
1287 */ |
1284 isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance ); |
1288 isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance ); |
1285 if ( isChanged ) { |
1289 if ( isChanged ) { |
1286 self.isWidgetUpdating = true; // suppress triggering another updateWidget |
1290 self.isWidgetUpdating = true; // Suppress triggering another updateWidget. |
1287 self.setting( r.data.instance ); |
1291 self.setting( r.data.instance ); |
1288 self.isWidgetUpdating = false; |
1292 self.isWidgetUpdating = false; |
1289 } else { |
1293 } else { |
1290 // no change was made, so stop the spinner now instead of when the preview would updates |
1294 // No change was made, so stop the spinner now instead of when the preview would updates. |
1291 self.container.removeClass( 'previewer-loading' ); |
1295 self.container.removeClass( 'previewer-loading' ); |
1292 } |
1296 } |
1293 |
1297 |
1294 if ( completeCallback ) { |
1298 if ( completeCallback ) { |
1295 completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } ); |
1299 completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } ); |
1296 } |
1300 } |
1297 } else { |
1301 } else { |
1298 // General error message |
1302 // General error message. |
1299 message = l10n.error; |
1303 message = l10n.error; |
1300 |
1304 |
1301 if ( r.data && r.data.message ) { |
1305 if ( r.data && r.data.message ) { |
1302 message = r.data.message; |
1306 message = r.data.message; |
1303 } |
1307 } |
1337 /** |
1341 /** |
1338 * @since 4.1.0 |
1342 * @since 4.1.0 |
1339 * |
1343 * |
1340 * @param {Boolean} expanded |
1344 * @param {Boolean} expanded |
1341 * @param {Object} [params] |
1345 * @param {Object} [params] |
1342 * @returns {Boolean} false if state already applied |
1346 * @return {Boolean} False if state already applied. |
1343 */ |
1347 */ |
1344 _toggleExpanded: api.Section.prototype._toggleExpanded, |
1348 _toggleExpanded: api.Section.prototype._toggleExpanded, |
1345 |
1349 |
1346 /** |
1350 /** |
1347 * @since 4.1.0 |
1351 * @since 4.1.0 |
1348 * |
1352 * |
1349 * @param {Object} [params] |
1353 * @param {Object} [params] |
1350 * @returns {Boolean} false if already expanded |
1354 * @return {Boolean} False if already expanded. |
1351 */ |
1355 */ |
1352 expand: api.Section.prototype.expand, |
1356 expand: api.Section.prototype.expand, |
1353 |
1357 |
1354 /** |
1358 /** |
1355 * Expand the widget form control |
1359 * Expand the widget form control |
1392 }, |
1396 }, |
1393 |
1397 |
1394 /** |
1398 /** |
1395 * Respond to change in the expanded state. |
1399 * Respond to change in the expanded state. |
1396 * |
1400 * |
1397 * @param {Boolean} expanded |
1401 * @param {boolean} expanded |
1398 * @param {Object} args merged on top of this.defaultActiveArguments |
1402 * @param {Object} args merged on top of this.defaultActiveArguments |
1399 */ |
1403 */ |
1400 onChangeExpanded: function ( expanded, args ) { |
1404 onChangeExpanded: function ( expanded, args ) { |
1401 var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn; |
1405 var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn; |
1402 |
1406 |
1403 self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI. |
1407 self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI. |
1404 if ( expanded ) { |
1408 if ( expanded ) { |
1405 self.embedWidgetContent(); |
1409 self.embedWidgetContent(); |
1406 } |
1410 } |
1407 |
1411 |
1408 // If the expanded state is unchanged only manipulate container expanded states |
1412 // If the expanded state is unchanged only manipulate container expanded states. |
1409 if ( args.unchanged ) { |
1413 if ( args.unchanged ) { |
1410 if ( expanded ) { |
1414 if ( expanded ) { |
1411 api.Control.prototype.expand.call( self, { |
1415 api.Control.prototype.expand.call( self, { |
1412 completeCallback: args.completeCallback |
1416 completeCallback: args.completeCallback |
1413 }); |
1417 }); |
1419 $inside = $widget.find( '.widget-inside:first' ); |
1423 $inside = $widget.find( '.widget-inside:first' ); |
1420 $toggleBtn = this.container.find( '.widget-top button.widget-action' ); |
1424 $toggleBtn = this.container.find( '.widget-top button.widget-action' ); |
1421 |
1425 |
1422 expandControl = function() { |
1426 expandControl = function() { |
1423 |
1427 |
1424 // Close all other widget controls before expanding this one |
1428 // Close all other widget controls before expanding this one. |
1425 api.control.each( function( otherControl ) { |
1429 api.control.each( function( otherControl ) { |
1426 if ( self.params.type === otherControl.params.type && self !== otherControl ) { |
1430 if ( self.params.type === otherControl.params.type && self !== otherControl ) { |
1427 otherControl.collapse(); |
1431 otherControl.collapse(); |
1428 } |
1432 } |
1429 } ); |
1433 } ); |
1524 }, |
1528 }, |
1525 |
1529 |
1526 /** |
1530 /** |
1527 * @private |
1531 * @private |
1528 * |
1532 * |
1529 * @param {Number} offset 1|-1 |
1533 * @param {number} offset 1|-1 |
1530 */ |
1534 */ |
1531 _moveWidgetByOne: function( offset ) { |
1535 _moveWidgetByOne: function( offset ) { |
1532 var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId; |
1536 var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId; |
1533 |
1537 |
1534 i = this.getWidgetSidebarPosition(); |
1538 i = this.getWidgetSidebarPosition(); |
1535 |
1539 |
1536 sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting; |
1540 sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting; |
1537 sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // clone |
1541 sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone. |
1538 adjacentWidgetId = sidebarWidgetIds[i + offset]; |
1542 adjacentWidgetId = sidebarWidgetIds[i + offset]; |
1539 sidebarWidgetIds[i + offset] = this.params.widget_id; |
1543 sidebarWidgetIds[i + offset] = this.params.widget_id; |
1540 sidebarWidgetIds[i] = adjacentWidgetId; |
1544 sidebarWidgetIds[i] = adjacentWidgetId; |
1541 |
1545 |
1542 sidebarWidgetsSetting( sidebarWidgetIds ); |
1546 sidebarWidgetsSetting( sidebarWidgetIds ); |
1543 }, |
1547 }, |
1544 |
1548 |
1545 /** |
1549 /** |
1546 * Toggle visibility of the widget move area |
1550 * Toggle visibility of the widget move area |
1547 * |
1551 * |
1548 * @param {Boolean} [showOrHide] |
1552 * @param {boolean} [showOrHide] |
1549 */ |
1553 */ |
1550 toggleWidgetMoveArea: function( showOrHide ) { |
1554 toggleWidgetMoveArea: function( showOrHide ) { |
1551 var self = this, $moveWidgetArea; |
1555 var self = this, $moveWidgetArea; |
1552 |
1556 |
1553 $moveWidgetArea = this.container.find( '.move-widget-area' ); |
1557 $moveWidgetArea = this.container.find( '.move-widget-area' ); |
1555 if ( typeof showOrHide === 'undefined' ) { |
1559 if ( typeof showOrHide === 'undefined' ) { |
1556 showOrHide = ! $moveWidgetArea.hasClass( 'active' ); |
1560 showOrHide = ! $moveWidgetArea.hasClass( 'active' ); |
1557 } |
1561 } |
1558 |
1562 |
1559 if ( showOrHide ) { |
1563 if ( showOrHide ) { |
1560 // reset the selected sidebar |
1564 // Reset the selected sidebar. |
1561 $moveWidgetArea.find( '.selected' ).removeClass( 'selected' ); |
1565 $moveWidgetArea.find( '.selected' ).removeClass( 'selected' ); |
1562 |
1566 |
1563 $moveWidgetArea.find( 'li' ).filter( function() { |
1567 $moveWidgetArea.find( 'li' ).filter( function() { |
1564 return $( this ).data( 'id' ) === self.params.sidebar_id; |
1568 return $( this ).data( 'id' ) === self.params.sidebar_id; |
1565 } ).addClass( 'selected' ); |
1569 } ).addClass( 'selected' ); |
1778 this.setting.bind( function( newWidgetIds, oldWidgetIds ) { |
1782 this.setting.bind( function( newWidgetIds, oldWidgetIds ) { |
1779 var widgetFormControls, removedWidgetIds, priority; |
1783 var widgetFormControls, removedWidgetIds, priority; |
1780 |
1784 |
1781 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); |
1785 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); |
1782 |
1786 |
1783 // Filter out any persistent widget IDs for widgets which have been deactivated |
1787 // Filter out any persistent widget IDs for widgets which have been deactivated. |
1784 newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) { |
1788 newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) { |
1785 var parsedWidgetId = parseWidgetId( newWidgetId ); |
1789 var parsedWidgetId = parseWidgetId( newWidgetId ); |
1786 |
1790 |
1787 return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } ); |
1791 return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } ); |
1788 } ); |
1792 } ); |
1795 } |
1799 } |
1796 |
1800 |
1797 return widgetFormControl; |
1801 return widgetFormControl; |
1798 } ); |
1802 } ); |
1799 |
1803 |
1800 // Sort widget controls to their new positions |
1804 // Sort widget controls to their new positions. |
1801 widgetFormControls.sort( function( a, b ) { |
1805 widgetFormControls.sort( function( a, b ) { |
1802 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), |
1806 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), |
1803 bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); |
1807 bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); |
1804 return aIndex - bIndex; |
1808 return aIndex - bIndex; |
1805 }); |
1809 }); |
1808 _( widgetFormControls ).each( function ( control ) { |
1812 _( widgetFormControls ).each( function ( control ) { |
1809 control.priority( priority ); |
1813 control.priority( priority ); |
1810 control.section( self.section() ); |
1814 control.section( self.section() ); |
1811 priority += 1; |
1815 priority += 1; |
1812 }); |
1816 }); |
1813 self.priority( priority ); // Make sure sidebar control remains at end |
1817 self.priority( priority ); // Make sure sidebar control remains at end. |
1814 |
1818 |
1815 // Re-sort widget form controls (including widgets form other sidebars newly moved here) |
1819 // Re-sort widget form controls (including widgets form other sidebars newly moved here). |
1816 self._applyCardinalOrderClassNames(); |
1820 self._applyCardinalOrderClassNames(); |
1817 |
1821 |
1818 // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated |
1822 // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated. |
1819 _( widgetFormControls ).each( function( widgetFormControl ) { |
1823 _( widgetFormControls ).each( function( widgetFormControl ) { |
1820 widgetFormControl.params.sidebar_id = self.params.sidebar_id; |
1824 widgetFormControl.params.sidebar_id = self.params.sidebar_id; |
1821 } ); |
1825 } ); |
1822 |
1826 |
1823 // Cleanup after widget removal |
1827 // Cleanup after widget removal. |
1824 _( removedWidgetIds ).each( function( removedWidgetId ) { |
1828 _( removedWidgetIds ).each( function( removedWidgetId ) { |
1825 |
1829 |
1826 // Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update |
1830 // Using setTimeout so that when moving a widget to another sidebar, |
|
1831 // the other sidebars_widgets settings get a chance to update. |
1827 setTimeout( function() { |
1832 setTimeout( function() { |
1828 var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase, |
1833 var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase, |
1829 widget, isPresentInAnotherSidebar = false; |
1834 widget, isPresentInAnotherSidebar = false; |
1830 |
1835 |
1831 // Check if the widget is in another sidebar |
1836 // Check if the widget is in another sidebar. |
1832 api.each( function( otherSetting ) { |
1837 api.each( function( otherSetting ) { |
1833 if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) { |
1838 if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) { |
1834 return; |
1839 return; |
1835 } |
1840 } |
1836 |
1841 |
1847 return; |
1852 return; |
1848 } |
1853 } |
1849 |
1854 |
1850 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId ); |
1855 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId ); |
1851 |
1856 |
1852 // Detect if widget control was dragged to another sidebar |
1857 // Detect if widget control was dragged to another sidebar. |
1853 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] ); |
1858 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] ); |
1854 |
1859 |
1855 // Delete any widget form controls for removed widgets |
1860 // Delete any widget form controls for removed widgets. |
1856 if ( removedControl && ! wasDraggedToAnotherSidebar ) { |
1861 if ( removedControl && ! wasDraggedToAnotherSidebar ) { |
1857 api.control.remove( removedControl.id ); |
1862 api.control.remove( removedControl.id ); |
1858 removedControl.container.remove(); |
1863 removedControl.container.remove(); |
1859 } |
1864 } |
1860 |
1865 |
1861 // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved |
1866 // Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved. |
1862 // This prevents the inactive widgets sidebar from overflowing with throwaway widgets |
1867 // This prevents the inactive widgets sidebar from overflowing with throwaway widgets. |
1863 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) { |
1868 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) { |
1864 inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice(); |
1869 inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice(); |
1865 inactiveWidgets.push( removedWidgetId ); |
1870 inactiveWidgets.push( removedWidgetId ); |
1866 api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() ); |
1871 api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() ); |
1867 } |
1872 } |
1868 |
1873 |
1869 // Make old single widget available for adding again |
1874 // Make old single widget available for adding again. |
1870 removedIdBase = parseWidgetId( removedWidgetId ).id_base; |
1875 removedIdBase = parseWidgetId( removedWidgetId ).id_base; |
1871 widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } ); |
1876 widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } ); |
1872 if ( widget && ! widget.get( 'is_multi' ) ) { |
1877 if ( widget && ! widget.get( 'is_multi' ) ) { |
1873 widget.set( 'is_disabled', false ); |
1878 widget.set( 'is_disabled', false ); |
1874 } |
1879 } |
1913 this.$controlSection.find( '.accordion-section-title' ).droppable({ |
1918 this.$controlSection.find( '.accordion-section-title' ).droppable({ |
1914 accept: '.customize-control-widget_form', |
1919 accept: '.customize-control-widget_form', |
1915 over: function() { |
1920 over: function() { |
1916 var section = api.section( self.section.get() ); |
1921 var section = api.section( self.section.get() ); |
1917 section.expand({ |
1922 section.expand({ |
1918 allowMultiple: true, // Prevent the section being dragged from to be collapsed |
1923 allowMultiple: true, // Prevent the section being dragged from to be collapsed. |
1919 completeCallback: function () { |
1924 completeCallback: function () { |
1920 // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed |
1925 // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed. |
1921 api.section.each( function ( otherSection ) { |
1926 api.section.each( function ( otherSection ) { |
1922 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { |
1927 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { |
1923 otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); |
1928 otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); |
1924 } |
1929 } |
1925 } ); |
1930 } ); |
2057 |
2062 |
2058 return formControls; |
2063 return formControls; |
2059 }, |
2064 }, |
2060 |
2065 |
2061 /** |
2066 /** |
2062 * @param {string} widgetId or an id_base for adding a previously non-existing widget |
2067 * @param {string} widgetId or an id_base for adding a previously non-existing widget. |
2063 * @returns {object|false} widget_form control instance, or false on error |
2068 * @return {Object|false} widget_form control instance, or false on error. |
2064 */ |
2069 */ |
2065 addWidget: function( widgetId ) { |
2070 addWidget: function( widgetId ) { |
2066 var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor, |
2071 var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor, |
2067 parsedWidgetId = parseWidgetId( widgetId ), |
2072 parsedWidgetId = parseWidgetId( widgetId ), |
2068 widgetNumber = parsedWidgetId.number, |
2073 widgetNumber = parsedWidgetId.number, |
2088 if ( widget.get( 'is_multi' ) ) { |
2093 if ( widget.get( 'is_multi' ) ) { |
2089 controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) { |
2094 controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) { |
2090 return m.replace( /__i__|%i%/g, widgetNumber ); |
2095 return m.replace( /__i__|%i%/g, widgetNumber ); |
2091 } ); |
2096 } ); |
2092 } else { |
2097 } else { |
2093 widget.set( 'is_disabled', true ); // Prevent single widget from being added again now |
2098 widget.set( 'is_disabled', true ); // Prevent single widget from being added again now. |
2094 } |
2099 } |
2095 |
2100 |
2096 $widget = $( controlHtml ); |
2101 $widget = $( controlHtml ); |
2097 |
2102 |
2098 controlContainer = $( '<li/>' ) |
2103 controlContainer = $( '<li/>' ) |
2099 .addClass( 'customize-control' ) |
2104 .addClass( 'customize-control' ) |
2100 .addClass( 'customize-control-' + controlType ) |
2105 .addClass( 'customize-control-' + controlType ) |
2101 .append( $widget ); |
2106 .append( $widget ); |
2102 |
2107 |
2103 // Remove icon which is visible inside the panel |
2108 // Remove icon which is visible inside the panel. |
2104 controlContainer.find( '> .widget-icon' ).remove(); |
2109 controlContainer.find( '> .widget-icon' ).remove(); |
2105 |
2110 |
2106 if ( widget.get( 'is_multi' ) ) { |
2111 if ( widget.get( 'is_multi' ) ) { |
2107 controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber ); |
2112 controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber ); |
2108 controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber ); |
2113 controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber ); |
2109 } |
2114 } |
2110 |
2115 |
2111 widgetId = controlContainer.find( '[name="widget-id"]' ).val(); |
2116 widgetId = controlContainer.find( '[name="widget-id"]' ).val(); |
2112 |
2117 |
2113 controlContainer.hide(); // to be slid-down below |
2118 controlContainer.hide(); // To be slid-down below. |
2114 |
2119 |
2115 settingId = 'widget_' + widget.get( 'id_base' ); |
2120 settingId = 'widget_' + widget.get( 'id_base' ); |
2116 if ( widget.get( 'is_multi' ) ) { |
2121 if ( widget.get( 'is_multi' ) ) { |
2117 settingId += '[' + widgetNumber + ']'; |
2122 settingId += '[' + widgetNumber + ']'; |
2118 } |
2123 } |
2119 controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); |
2124 controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); |
2120 |
2125 |
2121 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget) |
2126 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget). |
2122 isExistingWidget = api.has( settingId ); |
2127 isExistingWidget = api.has( settingId ); |
2123 if ( ! isExistingWidget ) { |
2128 if ( ! isExistingWidget ) { |
2124 settingArgs = { |
2129 settingArgs = { |
2125 transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh', |
2130 transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh', |
2126 previewer: this.setting.previewer |
2131 previewer: this.setting.previewer |
2127 }; |
2132 }; |
2128 setting = api.create( settingId, settingId, '', settingArgs ); |
2133 setting = api.create( settingId, settingId, '', settingArgs ); |
2129 setting.set( {} ); // mark dirty, changing from '' to {} |
2134 setting.set( {} ); // Mark dirty, changing from '' to {}. |
2130 } |
2135 } |
2131 |
2136 |
2132 controlConstructor = api.controlConstructor[controlType]; |
2137 controlConstructor = api.controlConstructor[controlType]; |
2133 widgetFormControl = new controlConstructor( settingId, { |
2138 widgetFormControl = new controlConstructor( settingId, { |
2134 settings: { |
2139 settings: { |
2163 otherSidebarWidgets.splice( i ); |
2168 otherSidebarWidgets.splice( i ); |
2164 otherSetting( otherSidebarWidgets ); |
2169 otherSetting( otherSidebarWidgets ); |
2165 } |
2170 } |
2166 } ); |
2171 } ); |
2167 |
2172 |
2168 // Add widget to this sidebar |
2173 // Add widget to this sidebar. |
2169 sidebarWidgets = this.setting().slice(); |
2174 sidebarWidgets = this.setting().slice(); |
2170 if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) { |
2175 if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) { |
2171 sidebarWidgets.push( widgetId ); |
2176 sidebarWidgets.push( widgetId ); |
2172 this.setting( sidebarWidgets ); |
2177 this.setting( sidebarWidgets ); |
2173 } |
2178 } |
2182 |
2187 |
2183 return widgetFormControl; |
2188 return widgetFormControl; |
2184 } |
2189 } |
2185 } ); |
2190 } ); |
2186 |
2191 |
2187 // Register models for custom panel, section, and control types |
2192 // Register models for custom panel, section, and control types. |
2188 $.extend( api.panelConstructor, { |
2193 $.extend( api.panelConstructor, { |
2189 widgets: api.Widgets.WidgetsPanel |
2194 widgets: api.Widgets.WidgetsPanel |
2190 }); |
2195 }); |
2191 $.extend( api.sectionConstructor, { |
2196 $.extend( api.sectionConstructor, { |
2192 sidebar: api.Widgets.SidebarSection |
2197 sidebar: api.Widgets.SidebarSection |
2198 |
2203 |
2199 /** |
2204 /** |
2200 * Init Customizer for widgets. |
2205 * Init Customizer for widgets. |
2201 */ |
2206 */ |
2202 api.bind( 'ready', function() { |
2207 api.bind( 'ready', function() { |
2203 // Set up the widgets panel |
2208 // Set up the widgets panel. |
2204 api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({ |
2209 api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({ |
2205 collection: api.Widgets.availableWidgets |
2210 collection: api.Widgets.availableWidgets |
2206 }); |
2211 }); |
2207 |
2212 |
2208 // Highlight widget control |
2213 // Highlight widget control. |
2209 api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl ); |
2214 api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl ); |
2210 |
2215 |
2211 // Open and focus widget control |
2216 // Open and focus widget control. |
2212 api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl ); |
2217 api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl ); |
2213 } ); |
2218 } ); |
2214 |
2219 |
2215 /** |
2220 /** |
2216 * Highlight a widget control. |
2221 * Highlight a widget control. |
2239 }, |
2244 }, |
2240 |
2245 |
2241 /** |
2246 /** |
2242 * Given a widget control, find the sidebar widgets control that contains it. |
2247 * Given a widget control, find the sidebar widgets control that contains it. |
2243 * @param {string} widgetId |
2248 * @param {string} widgetId |
2244 * @return {object|null} |
2249 * @return {Object|null} |
2245 */ |
2250 */ |
2246 api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) { |
2251 api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) { |
2247 var foundControl = null; |
2252 var foundControl = null; |
2248 |
2253 |
2249 // @todo this can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl() |
2254 // @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl(). |
2250 api.control.each( function( control ) { |
2255 api.control.each( function( control ) { |
2251 if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) { |
2256 if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) { |
2252 foundControl = control; |
2257 foundControl = control; |
2253 } |
2258 } |
2254 } ); |
2259 } ); |
2258 |
2263 |
2259 /** |
2264 /** |
2260 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it. |
2265 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it. |
2261 * |
2266 * |
2262 * @param {string} widgetId |
2267 * @param {string} widgetId |
2263 * @return {object|null} |
2268 * @return {Object|null} |
2264 */ |
2269 */ |
2265 api.Widgets.getWidgetFormControlForWidget = function( widgetId ) { |
2270 api.Widgets.getWidgetFormControlForWidget = function( widgetId ) { |
2266 var foundControl = null; |
2271 var foundControl = null; |
2267 |
2272 |
2268 // @todo We can just use widgetIdToSettingId() here |
2273 // @todo We can just use widgetIdToSettingId() here. |
2269 api.control.each( function( control ) { |
2274 api.control.each( function( control ) { |
2270 if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) { |
2275 if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) { |
2271 foundControl = control; |
2276 foundControl = control; |
2272 } |
2277 } |
2273 } ); |
2278 } ); |
2340 matches = widgetId.match( /^(.+)-(\d+)$/ ); |
2345 matches = widgetId.match( /^(.+)-(\d+)$/ ); |
2341 if ( matches ) { |
2346 if ( matches ) { |
2342 parsed.id_base = matches[1]; |
2347 parsed.id_base = matches[1]; |
2343 parsed.number = parseInt( matches[2], 10 ); |
2348 parsed.number = parseInt( matches[2], 10 ); |
2344 } else { |
2349 } else { |
2345 // likely an old single widget |
2350 // Likely an old single widget. |
2346 parsed.id_base = widgetId; |
2351 parsed.id_base = widgetId; |
2347 } |
2352 } |
2348 |
2353 |
2349 return parsed; |
2354 return parsed; |
2350 } |
2355 } |
2351 |
2356 |
2352 /** |
2357 /** |
2353 * @param {String} widgetId |
2358 * @param {string} widgetId |
2354 * @returns {String} settingId |
2359 * @return {string} settingId |
2355 */ |
2360 */ |
2356 function widgetIdToSettingId( widgetId ) { |
2361 function widgetIdToSettingId( widgetId ) { |
2357 var parsed = parseWidgetId( widgetId ), settingId; |
2362 var parsed = parseWidgetId( widgetId ), settingId; |
2358 |
2363 |
2359 settingId = 'widget_' + parsed.id_base; |
2364 settingId = 'widget_' + parsed.id_base; |