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
This commit is contained in:
koopersmith 2012-04-03 22:04:40 +00:00
parent 32e1568691
commit 272d6daac2
2 changed files with 157 additions and 215 deletions

View File

@ -2,7 +2,7 @@ if ( typeof wp === 'undefined' )
var wp = {}; var wp = {};
(function( exports, $ ){ (function( exports, $ ){
var api, extend, ctor, inherits, ready, var api, extend, ctor, inherits,
slice = Array.prototype.slice; slice = Array.prototype.slice;
/* ===================================================================== /* =====================================================================
@ -66,34 +66,7 @@ if ( typeof wp === 'undefined' )
return child; 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 = {};
// api = function( callback, context ) {
// if ( $.isFunction( callback ) ) {
// if ( context )
// callback = $.proxy( callback, context );
// ready.add( callback );
//
// return api;
// }
// }
/* ===================================================================== /* =====================================================================
* Base class. * Base class.
@ -156,6 +129,8 @@ if ( typeof wp === 'undefined' )
this.callbacks = $.Callbacks(); this.callbacks = $.Callbacks();
$.extend( this, options || {} ); $.extend( this, options || {} );
this.set = $.proxy( this.set, this );
}, },
/* /*
@ -173,6 +148,7 @@ if ( typeof wp === 'undefined' )
set: function( to ) { set: function( to ) {
var from = this._value; var from = this._value;
to = this._setter.apply( this, arguments );
to = this.validate( to ); to = this.validate( to );
// Bail if the sanitized value is null or unchanged. // Bail if the sanitized value is null or unchanged.
@ -186,6 +162,22 @@ if ( typeof wp === 'undefined' )
return this; 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 ) { validate: function( value ) {
return value; return value;
}, },
@ -200,183 +192,48 @@ if ( typeof wp === 'undefined' )
return this; return this;
}, },
/* link: function() { // values*
* 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() {
var set = this.set; var set = this.set;
$.each( arguments, function() {
$.each( this.links, function( key, value ) { this.bind( set );
value.unbind( set );
}); });
return this;
},
delete this.links; unlink: function() { // values*
this.set = this.set.original; 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; return this;
} }
}); });
api.ensure = function( element ) { api.Values = api.Class.extend({
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({
defaultConstructor: api.Value, defaultConstructor: api.Value,
initialize: function( options ) { initialize: function( options ) {
api.Value.prototype.initialize.call( this, {}, options || {} ); $.extend( this, options || {} );
this._value = {};
this._deferreds = {}; this._deferreds = {};
}, },
@ -400,7 +257,7 @@ if ( typeof wp === 'undefined' )
return this.value( id ); return this.value( id );
this._value[ id ] = value; this._value[ id ] = value;
this._value[ id ]._parent = this._value; this._value[ id ].parent = this;
if ( this._deferreds[ id ] ) if ( this._deferreds[ id ] )
this._deferreds[ id ].resolve(); 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() { api.Values.prototype[ method ] = function() {
return this.pass( method, arguments ); 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. * Messenger for postMessage.
* ===================================================================== */ * ===================================================================== */
api.Messenger = api.Class.extend({ 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 ) { initialize: function( url, targetWindow, options ) {
$.extend( this, options || {} ); $.extend( this, options || {} );
this.add( 'url', url ); url = this.add( 'url', url );
this.add( 'targetWindow', targetWindow || null ); this.add( 'targetWindow', targetWindow || null );
this.add( 'origin' ).link( 'url', function( url ) { this.add( 'origin', url() ).link( url ).setter( function( to ) {
return url().replace( /([^:]+:\/\/[^\/]+).*/, '$1' ); return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' );
}); });
this.topics = {}; this.topics = {};
$.receiveMessage( $.proxy( this.receive, this ), this.origin() || null ); $.receiveMessage( $.proxy( this.receive, this ), this.origin() || null );
}, },
receive: function( event ) { receive: function( event ) {
var message; var message;
@ -507,6 +453,7 @@ if ( typeof wp === 'undefined' )
if ( message && message.id && message.data && this.topics[ message.id ] ) if ( message && message.id && message.data && this.topics[ message.id ] )
this.topics[ message.id ].fireWith( this, [ message.data ]); this.topics[ message.id ].fireWith( this, [ message.data ]);
}, },
send: function( id, data ) { send: function( id, data ) {
var message; var message;
@ -516,10 +463,12 @@ if ( typeof wp === 'undefined' )
message = JSON.stringify({ id: id, data: data }); message = JSON.stringify({ id: id, data: data });
$.postMessage( message, this.url(), this.targetWindow() ); $.postMessage( message, this.url(), this.targetWindow() );
}, },
bind: function( id, callback ) { bind: function( id, callback ) {
var topic = this.topics[ id ] || ( this.topics[ id ] = $.Callbacks() ); var topic = this.topics[ id ] || ( this.topics[ id ] = $.Callbacks() );
topic.add( callback ); topic.add( callback );
}, },
unbind: function( id, callback ) { unbind: function( id, callback ) {
if ( this.topics[ id ] ) if ( this.topics[ id ] )
this.topics[ id ].remove( callback ); this.topics[ id ].remove( callback );

View File

@ -4,7 +4,7 @@
/* /*
* @param options * @param options
* - previewer - The Previewer instance to sync with. * - 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({ api.Setting = api.Value.extend({
initialize: function( id, value, options ) { initialize: function( id, value, options ) {
@ -24,12 +24,10 @@
element.appendTo( this.previewer.form ); element.appendTo( this.previewer.form );
this.element = new api.Element( element ); this.element = new api.Element( element );
this.element.link( this ); this.sync( this.element );
this.link( this.element ); this.bind( this.preview );
this.bind( this.sync );
}, },
sync: function() { preview: function() {
switch ( this.method ) { switch ( this.method ) {
case 'refresh': case 'refresh':
return this.previewer.refresh(); return this.previewer.refresh();
@ -88,9 +86,8 @@
api( node.data('customizeSettingLink'), function( setting ) { api( node.data('customizeSettingLink'), function( setting ) {
var element = new api.Element( node ); var element = new api.Element( node );
control.elements.push( element ); control.elements.push( element );
element.link( setting ).bind( function( to ) { element.sync( setting );
setting( to ); element.set( setting() );
});
}); });
}); });
}, },
@ -122,10 +119,6 @@
this.setting.bind( update ); this.setting.bind( update );
update( this.setting() ); 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({ api.UploadControl = api.Control.extend({
@ -400,7 +393,7 @@
api.control( 'display_header_text', function( control ) { api.control( 'display_header_text', function( control ) {
var last = ''; var last = '';
control.elements[0].unlink(); control.elements[0].unsync( api( 'header_textcolor' ) );
control.element = new api.Element( control.container.find('input') ); control.element = new api.Element( control.container.find('input') );
control.element.set( 'blank' !== control.setting() ); control.element.set( 'blank' !== control.setting() );