From 272d6daac2c719093d6a25201b60cc5d3d4c86c3 Mon Sep 17 00:00:00 2001 From: koopersmith Date: Tue, 3 Apr 2012 22:04:40 +0000 Subject: [PATCH] Theme Customizer: Improve data binding in wp.customize.Value and wp.customize.Values. see #19910. * Replace the convoluted wp.customize.Value.link method with a simple shortcut for direct binding. * Add wp.customize.Value.sync for bidirectional linking. * Add wp.customize.Value.setter for handling compound values (instead of using wp.customize.Value.link). git-svn-id: http://svn.automattic.com/wordpress/trunk@20344 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/js/customize-base.dev.js | 351 ++++++++++------------- wp-includes/js/customize-controls.dev.js | 21 +- 2 files changed, 157 insertions(+), 215 deletions(-) diff --git a/wp-includes/js/customize-base.dev.js b/wp-includes/js/customize-base.dev.js index ca3a2376d..73959e370 100644 --- a/wp-includes/js/customize-base.dev.js +++ b/wp-includes/js/customize-base.dev.js @@ -2,7 +2,7 @@ if ( typeof wp === 'undefined' ) var wp = {}; (function( exports, $ ){ - var api, extend, ctor, inherits, ready, + var api, extend, ctor, inherits, slice = Array.prototype.slice; /* ===================================================================== @@ -66,34 +66,7 @@ if ( typeof wp === 'undefined' ) return child; }; - /* ===================================================================== - * customize function. - * ===================================================================== */ - ready = $.Callbacks( 'once memory' ); - - /* - * Sugar for main customize function. Supports several signatures. - * - * customize( callback, [context] ); - * Binds a callback to be fired when the customizer is ready. - * - callback, function - * - context, object - * - * customize( setting ); - * Fetches a setting object by ID. - * - setting, string - The setting ID. - * - */ api = {}; - // api = function( callback, context ) { - // if ( $.isFunction( callback ) ) { - // if ( context ) - // callback = $.proxy( callback, context ); - // ready.add( callback ); - // - // return api; - // } - // } /* ===================================================================== * Base class. @@ -156,6 +129,8 @@ if ( typeof wp === 'undefined' ) this.callbacks = $.Callbacks(); $.extend( this, options || {} ); + + this.set = $.proxy( this.set, this ); }, /* @@ -173,6 +148,7 @@ if ( typeof wp === 'undefined' ) set: function( to ) { var from = this._value; + to = this._setter.apply( this, arguments ); to = this.validate( to ); // Bail if the sanitized value is null or unchanged. @@ -186,6 +162,22 @@ if ( typeof wp === 'undefined' ) return this; }, + _setter: function( to ) { + return to; + }, + + setter: function( callback ) { + this._setter = callback; + this.set( this.get() ); + return this; + }, + + resetSetter: function() { + this._setter = this.constructor.prototype._setter; + this.set( this.get() ); + return this; + }, + validate: function( value ) { return value; }, @@ -200,183 +192,48 @@ if ( typeof wp === 'undefined' ) return this; }, - /* - * Allows the creation of composite values. - * Overrides the native link method (can be reverted with `unlink`). - */ - link: function() { - var keys = slice.call( arguments ), - callback = keys.pop(), - self = this, - set, key, active; - - if ( this.links ) - this.unlink(); - - this.links = []; - - // Single argument means a direct binding. - if ( ! keys.length ) { - keys = [ callback ]; - callback = function( value, to ) { - return to; - }; - } - - while ( key = keys.shift() ) { - if ( this._parent && $.type( key ) == 'string' ) - this.links.push( this._parent[ key ] ); - else - this.links.push( key ); - } - - // Replace this.set with the assignment function. - set = function() { - var args, result; - - // If we call set from within the assignment function, - // pass the arguments to the original set. - if ( active ) - return self.set.original.apply( self, arguments ); - - active = true; - - args = self.links.concat( slice.call( arguments ) ); - result = callback.apply( self, args ); - - active = false; - - if ( typeof result !== 'undefined' ) - self.set.original.call( self, result ); - }; - - set.original = this.set; - this.set = set; - - // Bind the new function to the master values. - $.each( this.links, function( key, value ) { - value.bind( self.set ); - }); - - this.set( this.get() ); - - return this; - }, - - unlink: function() { + link: function() { // values* var set = this.set; - - $.each( this.links, function( key, value ) { - value.unbind( set ); + $.each( arguments, function() { + this.bind( set ); }); + return this; + }, - delete this.links; - this.set = this.set.original; + unlink: function() { // values* + var set = this.set; + $.each( arguments, function() { + this.unbind( set ); + }); + return this; + }, + + sync: function() { // values* + var that = this; + $.each( arguments, function() { + that.link( this ); + this.link( that ); + }); + return this; + }, + + unsync: function() { // values* + var that = this; + $.each( arguments, function() { + that.unlink( this ); + this.unlink( that ); + }); return this; } }); - api.ensure = function( element ) { - return typeof element == 'string' ? $( element ) : element; - }; - - api.Element = api.Value.extend({ - initialize: function( element, options ) { - var self = this, - synchronizer = api.Element.synchronizer.html, - type, update, refresh; - - this.element = api.ensure( element ); - this.events = ''; - - if ( this.element.is('input, select, textarea') ) { - this.events += 'change'; - synchronizer = api.Element.synchronizer.val; - - if ( this.element.is('input') ) { - type = this.element.prop('type'); - if ( api.Element.synchronizer[ type ] ) - synchronizer = api.Element.synchronizer[ type ]; - if ( 'text' === type || 'password' === type ) - this.events += ' keyup'; - } - } - - api.Value.prototype.initialize.call( this, null, $.extend( options || {}, synchronizer ) ); - this._value = this.get(); - - update = this.update; - refresh = this.refresh; - - this.update = function( to ) { - if ( to !== refresh.call( self ) ) - update.apply( this, arguments ); - }; - this.refresh = function() { - self.set( refresh.call( self ) ); - }; - - this.bind( this.update ); - this.element.bind( this.events, this.refresh ); - }, - - find: function( selector ) { - return $( selector, this.element ); - }, - - refresh: function() {}, - update: function() {} - }); - - api.Element.synchronizer = {}; - - $.each( [ 'html', 'val' ], function( i, method ) { - api.Element.synchronizer[ method ] = { - update: function( to ) { - this.element[ method ]( to ); - }, - refresh: function() { - return this.element[ method ](); - } - }; - }); - - api.Element.synchronizer.checkbox = { - update: function( to ) { - this.element.prop( 'checked', to ); - }, - refresh: function() { - return this.element.prop( 'checked' ); - } - }; - - api.Element.synchronizer.radio = { - update: function( to ) { - this.element.filter( function() { - return this.value === to; - }).prop( 'checked', true ); - }, - refresh: function() { - return this.element.filter( ':checked' ).val(); - } - }; - - api.ValueFactory = function( constructor ) { - constructor = constructor || api.Value; - - return function( key ) { - var args = slice.call( arguments, 1 ); - this[ key ] = new constructor( api.Class.applicator, args ); - this[ key ]._parent = this; - return this[ key ]; - }; - }; - - api.Values = api.Value.extend({ + api.Values = api.Class.extend({ defaultConstructor: api.Value, initialize: function( options ) { - api.Value.prototype.initialize.call( this, {}, options || {} ); + $.extend( this, options || {} ); + + this._value = {}; this._deferreds = {}; }, @@ -400,7 +257,7 @@ if ( typeof wp === 'undefined' ) return this.value( id ); this._value[ id ] = value; - this._value[ id ]._parent = this._value; + this._value[ id ].parent = this; if ( this._deferreds[ id ] ) this._deferreds[ id ].resolve(); @@ -469,32 +326,121 @@ if ( typeof wp === 'undefined' ) } }); - $.each( [ 'get', 'bind', 'unbind', 'link', 'unlink' ], function( i, method ) { + $.each( [ 'get', 'bind', 'unbind', 'link', 'unlink', 'sync', 'unsync', 'setter', 'resetSetter' ], function( i, method ) { api.Values.prototype[ method ] = function() { return this.pass( method, arguments ); }; }); + api.ensure = function( element ) { + return typeof element == 'string' ? $( element ) : element; + }; + + api.Element = api.Value.extend({ + initialize: function( element, options ) { + var self = this, + synchronizer = api.Element.synchronizer.html, + type, update, refresh; + + this.element = api.ensure( element ); + this.events = ''; + + if ( this.element.is('input, select, textarea') ) { + this.events += 'change'; + synchronizer = api.Element.synchronizer.val; + + if ( this.element.is('input') ) { + type = this.element.prop('type'); + if ( api.Element.synchronizer[ type ] ) + synchronizer = api.Element.synchronizer[ type ]; + if ( 'text' === type || 'password' === type ) + this.events += ' keyup'; + } + } + + api.Value.prototype.initialize.call( this, null, $.extend( options || {}, synchronizer ) ); + this._value = this.get(); + + update = this.update; + refresh = this.refresh; + + this.update = function( to ) { + if ( to !== refresh.call( self ) ) + update.apply( this, arguments ); + }; + this.refresh = function() { + self.set( refresh.call( self ) ); + }; + + this.bind( this.update ); + this.element.bind( this.events, this.refresh ); + }, + + find: function( selector ) { + return $( selector, this.element ); + }, + + refresh: function() {}, + + update: function() {} + }); + + api.Element.synchronizer = {}; + + $.each( [ 'html', 'val' ], function( i, method ) { + api.Element.synchronizer[ method ] = { + update: function( to ) { + this.element[ method ]( to ); + }, + refresh: function() { + return this.element[ method ](); + } + }; + }); + + api.Element.synchronizer.checkbox = { + update: function( to ) { + this.element.prop( 'checked', to ); + }, + refresh: function() { + return this.element.prop( 'checked' ); + } + }; + + api.Element.synchronizer.radio = { + update: function( to ) { + this.element.filter( function() { + return this.value === to; + }).prop( 'checked', true ); + }, + refresh: function() { + return this.element.filter( ':checked' ).val(); + } + }; + /* ===================================================================== * Messenger for postMessage. * ===================================================================== */ api.Messenger = api.Class.extend({ - add: api.ValueFactory(), + add: function( key, initial, options ) { + return this[ key ] = new api.Value( initial, options ); + }, initialize: function( url, targetWindow, options ) { $.extend( this, options || {} ); - this.add( 'url', url ); + url = this.add( 'url', url ); this.add( 'targetWindow', targetWindow || null ); - this.add( 'origin' ).link( 'url', function( url ) { - return url().replace( /([^:]+:\/\/[^\/]+).*/, '$1' ); + this.add( 'origin', url() ).link( url ).setter( function( to ) { + return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' ); }); this.topics = {}; $.receiveMessage( $.proxy( this.receive, this ), this.origin() || null ); }, + receive: function( event ) { var message; @@ -507,6 +453,7 @@ if ( typeof wp === 'undefined' ) if ( message && message.id && message.data && this.topics[ message.id ] ) this.topics[ message.id ].fireWith( this, [ message.data ]); }, + send: function( id, data ) { var message; @@ -516,10 +463,12 @@ if ( typeof wp === 'undefined' ) message = JSON.stringify({ id: id, data: data }); $.postMessage( message, this.url(), this.targetWindow() ); }, + bind: function( id, callback ) { var topic = this.topics[ id ] || ( this.topics[ id ] = $.Callbacks() ); topic.add( callback ); }, + unbind: function( id, callback ) { if ( this.topics[ id ] ) this.topics[ id ].remove( callback ); diff --git a/wp-includes/js/customize-controls.dev.js b/wp-includes/js/customize-controls.dev.js index 55949cb53..392573374 100644 --- a/wp-includes/js/customize-controls.dev.js +++ b/wp-includes/js/customize-controls.dev.js @@ -4,7 +4,7 @@ /* * @param options * - previewer - The Previewer instance to sync with. - * - method - The method to use for syncing. Supports 'refresh' and 'postMessage'. + * - method - The method to use for previewing. Supports 'refresh' and 'postMessage'. */ api.Setting = api.Value.extend({ initialize: function( id, value, options ) { @@ -24,12 +24,10 @@ element.appendTo( this.previewer.form ); this.element = new api.Element( element ); - this.element.link( this ); - this.link( this.element ); - - this.bind( this.sync ); + this.sync( this.element ); + this.bind( this.preview ); }, - sync: function() { + preview: function() { switch ( this.method ) { case 'refresh': return this.previewer.refresh(); @@ -88,9 +86,8 @@ api( node.data('customizeSettingLink'), function( setting ) { var element = new api.Element( node ); control.elements.push( element ); - element.link( setting ).bind( function( to ) { - setting( to ); - }); + element.sync( setting ); + element.set( setting() ); }); }); }, @@ -122,10 +119,6 @@ this.setting.bind( update ); update( this.setting() ); } - // , - // validate: function( to ) { - // return /^[a-fA-F0-9]{3}([a-fA-F0-9]{3})?$/.test( to ) ? to : null; - // } }); api.UploadControl = api.Control.extend({ @@ -400,7 +393,7 @@ api.control( 'display_header_text', function( control ) { var last = ''; - control.elements[0].unlink(); + control.elements[0].unsync( api( 'header_textcolor' ) ); control.element = new api.Element( control.container.find('input') ); control.element.set( 'blank' !== control.setting() );