', { class : module.control.css_attr.single_item, 'data-id' : item_model_for_template_injection.id, id : item_model_for_template_injection.id } );
//append the item view to the first module view wrapper
//!!note : => there could be additional sub view wrapper inside !!
//$( '.' + module.control.css_attr.items_wrapper , module.container).first().append( $_view_el );
// module.itemsWrapper has been stored as a $ var in module initialize() when the tmpl has been embedded
module.itemsWrapper.append( $_view_el );
if ( module.isMultiItem() ) {
var _template_selector;
// Do we have view content template script?
// if yes, let's use it <= Old way
// Otherwise let's fetch the html template from the server
if ( ! _.isEmpty( module.rudItemPart ) ) {
_template_selector = module.getTemplateSelectorPart( 'rudItemPart', item_model_for_template_injection );
//do we have view template script?
if ( 1 > $( '#tmpl-' + _template_selector ).length ) {
dfd.reject( 'Missing template for item ' + item.id + '. The provided template script has no been found : #tmpl-' + _template_selector );
}
appendAndResolve( wp.template( _template_selector )( $.extend( item_model_for_template_injection, { is_sortable : module.sortable } ) ) );
} else {
// allow plugin to alter the ajax params before fetching
var requestParams = {
tmpl : 'rud-item-part',
module_type: 'all_modules',
module_id : module.id,
control_id : module.control.id
};
item.trigger( 'item-wrapper-tmpl-params-before-fetching', requestParams );
// Let's check if the filtered requested params can find a match of a printed tmpl of the module
// this filter 'item-wrapper-tmpl-params-before-fetching', is used in the widget zone module of the Hueman theme (june 2018 )
// it allows us to assign a specific template for the built-in widget zones
if ( ! _.isEmpty( module[ requestParams.tmpl ] ) ) {
_template_selector = module.getTemplateSelectorPart( requestParams.tmpl, item_model_for_template_injection );
//do we have view template script?
if ( 1 > $( '#tmpl-' + _template_selector ).length ) {
dfd.reject( 'Missing template for item ' + item.id + '. The provided template script has no been found : #tmpl-' + _template_selector );
}
appendAndResolve( wp.template( _template_selector )( item_model_for_template_injection ) );
} else {
api.CZR_Helpers.getModuleTmpl( requestParams ).done( function( _serverTmpl_ ) {
//console.log( 'renderItemWrapper => success response =>', module.id, _serverTmpl_);
appendAndResolve( api.CZR_Helpers.parseTemplate( _serverTmpl_ )( $.extend( item_model_for_template_injection, { is_sortable : module.sortable } ) ) );
}).fail( function( _r_ ) {
//console.log( 'renderItemWrapper => fail response =>', _r_);
dfd.reject( 'renderItemWrapper => Problem when fetching the rud-item-part tmpl from server for module : '+ module.id );
});
}
}
} else {//if ( module.isMultiItem() ) {}
appendAndResolve();
}
return dfd.promise();
},
// fired when item is ready and embedded
// define the item view DOM event map
// bind actions when the item is embedded
itemWrapperViewSetup : function( _item_model_ ) {
var item = this,
module = this.module;
// _item_model_ = item() || item.initial_item_model;//could not be set yet
// Let's create a deep copy now
item_model = item() || item.initial_item_model;//$.extend( true, {}, _item_model_ );
// always write the title
item.writeItemViewTitle();
// When do we render the item content ?
// If this is a multi-item module, let's render each item content when they are expanded.
// In the case of a single item module, we can render the item content now.
var _updateItemContentDeferred = function( $_item_content, to, from ) {
//update the $.Deferred state
if ( ! _.isUndefined( $_item_content ) && false !== $_item_content.length ) {
item.contentContainer = $_item_content;
// The 'contentRendered' event triggers the api.CZR_Helpers.setupInputCollectionFromDOM.call( item );
item.trigger( 'contentRendered', { item_content : $_item_content } );
item.toggleItemExpansion( to, item.module.isMultiItem() ? 150 : 0 );//the second param is the duration
item.cleanLoader();
}
else {
throw new Error( 'Module : ' + item.module.id + ', the item content has not been rendered for ' + item.id );
}
};
// MULTI-ITEM MODULE
if ( item.module.isMultiItem() ) {
item.viewState.callbacks.add( function( to, from ) {
//viewState can take 3 states : expanded, expanded_noscroll, closed
var _isExpanded = -1 !== to.indexOf( 'expanded' );
//If this module has mod Opt, always close the opt pane on view state change
if ( module.hasModOpt() && _isExpanded ) {
api.czr_ModOptVisible( false, {
module : module,//the current module for which the modOpt is being expanded
focus : false//the id of the tab we want to focus on
});
}
if ( _isExpanded ) {
//item already rendered ?
if ( _.isObject( item.contentContainer ) && false !== item.contentContainer.length ) {
//toggle on view state change
item.toggleItemExpansion(to);
} else {
item.printLoader();
item.renderItemContent( item() || item.initial_item_model )
.done( function( $_item_content ) {
//introduce a small delay to give some times to the modules to be printed.
//@todo : needed ?
//_updateItemContentDeferred = _.debounce(_updateItemContentDeferred, 50 );
_updateItemContentDeferred( $_item_content, to, from );
})
.fail( function( _r_ ) {
api.errorLog( "multi-item module => failed item.renderItemContent for module : " + module.id, _r_ );
});
}
} else {
//toggle on view state change
item.toggleItemExpansion( to ).done( function() {
if ( _.isObject( item.contentContainer ) && false !== item.contentContainer.length ) {
item.trigger( 'beforeContenRemoved' );
//Removes DOM input nodes
$( '.' + module.control.css_attr.item_content, item.container ).children().each( function() {
$(this).remove();
});
//clean any other content like a commented html markup
$( '.' + module.control.css_attr.item_content, item.container ).html('');
//reset the contentContainer property
item.contentContainer = null;
//will remove the input collection values
item.trigger( 'contentRemoved' );
}
});
}
});
}
// SINGLE ITEM MODULE
else {
//react to the item state changes
item.viewState.callbacks.add( function( to, from ) {
//toggle on view state change
item.toggleItemExpansion.apply( item, [ to, 0 ] );
});
item.printLoader();
//renderview content now for a single item module
item.renderItemContent( item_model )
.done( function( $_item_content ) {
_updateItemContentDeferred( $_item_content, true );
//item.viewState.set('expanded');
})
.fail( function( _r_ ) {
api.errare( "mono-item module => failed item.renderItemContent for module : " + module.id, _r_ );
});
}
//DOM listeners for the user action in item view wrapper
api.CZR_Helpers.setupDOMListeners(
item.userEventMap(),//actions to execute
{ model:item_model, dom_el:item.container },//model + dom scope
item //instance where to look for the cb methods
);
//Listen to the remove dialog state
item.removeDialogVisible.bind( function( visible ) {
var module = item.module,
$_alert_el = $( '.' + module.control.css_attr.remove_alert_wrapper, item.container ).first();
//first close all open items views and dialogs
if ( visible )
module.closeAllItems();
//Close Mod opts if any
if ( visible && module.hasModOpt() ) {
api.czr_ModOptVisible( false, {
module : module,//the current module for which the modOpt is being expanded
focus : false//the id of the tab we want to focus on
});
}
//Close Pre item dialog
if ( visible && _.has( module, 'preItem' ) ) {
module.preItemExpanded(false);
}
//then close any other open remove dialog in the item container
$('.' + module.control.css_attr.remove_alert_wrapper, item.container ).not( $_alert_el ).each( function() {
if ( $(this).hasClass( 'open' ) ) {
$(this).slideToggle( {
duration : 200,
done : function() {
$(this).toggleClass('open' , false );
//deactivate the icons
$(this).siblings().find('.' + module.control.css_attr.display_alert_btn).toggleClass( 'active' , false );
}
} );
}
});
//print the html if dialod is expanded
if ( visible ) {
// Do we have view content template script?
// if yes, let's use it <= Old way
// Otherwise let's fetch the html template from the server
if ( ! _.isEmpty( module.alertPart ) ) {
if ( 1 > $( '#tmpl-' + module.alertPart ).length || _.isEmpty( item.container ) ) {
api.errare( 'No removal alert template available for items in module :' + module.id );
return;
}
$_alert_el.html( wp.template( module.alertPart )( { title : ( item().title || item.id ) } ) );
item.trigger( 'remove-dialog-rendered');
} else {
api.CZR_Helpers.getModuleTmpl( {
tmpl : 'rud-item-alert-part',
module_type: 'all_modules',
module_id : module.id,
control_id : module.control.id
} ).done( function( _serverTmpl_ ) {
//console.log( 'item.removeDialogVisible => success response =>', module.id, _serverTmpl_);
$_alert_el.html( api.CZR_Helpers.parseTemplate( _serverTmpl_ )( { title : ( item().title || item.id ) } ) );
item.trigger( 'remove-dialog-rendered');
}).fail( function( _r_ ) {
//console.log( 'item.removeDialogVisible => fail response =>', _r_);
api.errare( 'item.removeDialogVisible => Problem when fetching the tmpl from server for module : '+ module.id, _r_ );
});
}
}
//Slide it
var _slideComplete = function( visible ) {
$_alert_el.toggleClass( 'open' , visible );
//set the active class of the clicked icon
item.container.find('.' + module.control.css_attr.display_alert_btn ).toggleClass( 'active', visible );
//adjust scrolling to display the entire dialog block
if ( visible )
module._adjustScrollExpandedBlock( item.container );
};
if ( visible ) {
$_alert_el.stop( true, true ).slideDown( 200, function() { _slideComplete( visible ); } );
} else {
$_alert_el.stop( true, true ).slideUp( 200, function() { _slideComplete( visible ); } );
}
});//item.removeDialogVisible.bind()
},//itemWrapperViewSetup
//renders saved items views and attach event handlers
//the saved item look like :
//array[ { id : 'sidebar-one', title : 'A Title One' }, {id : 'sidebar-two', title : 'A Title Two' }]
renderItemContent : function( _item_model_ ) {
//=> an array of objects
var item = this,
module = this.module,
dfd = $.Deferred();
// Create a deep copy of the item, so we can inject custom properties before parsing the template, without affecting the original item
var item_model_for_template_injection = $.extend( true, {}, _item_model_ || item() );
// allow plugin to alter the item_model before template injection
item.trigger( 'item-model-before-item-content-template-injection', item_model_for_template_injection );
var appendAndResolve = function( _tmpl_ ) {
//do we have an html template ?
if ( _.isEmpty( _tmpl_ ) ) {
dfd.reject( 'renderItemContent => Missing html template for module : '+ module.id );
}
var $itemContentWrapper = $( '.' + module.control.css_attr.item_content, item.container );
// append the view content
$( _tmpl_ ).appendTo( $itemContentWrapper );
dfd.resolve( $itemContentWrapper );
};//appendAndResolve
// Do we have view content template script?
// if yes, let's use it <= Old way
// Otherwise let's fetch the html template from the server
if ( ! _.isEmpty( module.itemInputList ) || _.isFunction( module.itemInputList ) ) {
var tmplSelectorSuffix = module.getTemplateSelectorPart( 'itemInputList', item_model_for_template_injection );
if ( 1 > $( '#tmpl-' + tmplSelectorSuffix ).length ) {
dfd.reject( 'renderItemContent => No itemInputList content template defined for module ' + module.id + '. The template script id should be : #tmpl-' + tmplSelectorSuffix );
} else {
appendAndResolve( wp.template( tmplSelectorSuffix )( $.extend( item_model_for_template_injection, { control_id : module.control.id } ) ) );
}
} else {
var requestParams = {
tmpl : 'item-inputs',
module_type: module.module_type,
module_id : module.id,
control_id : module.control.id,
item_model : item_model_for_template_injection
};
// allow plugins to filter the query param before fetching the template for item content
module.trigger( 'filter-request-params-before-fetching-for-item-content-tmpl', requestParams );
api.CZR_Helpers.getModuleTmpl( requestParams ).done( function( _serverTmpl_ ) {
//console.log( 'renderItemContent => success response =>', _serverTmpl_);
appendAndResolve( api.CZR_Helpers.parseTemplate( _serverTmpl_ )( $.extend( item_model_for_template_injection, { control_id : module.control.id } ) ) );
}).fail( function( _r_ ) {
//console.log( 'renderItemContent => fail response =>', _r_);
dfd.reject( _r_ );
});
}
return dfd.promise();
},
//fired in setupItemListeners
writeItemViewTitle : function( item_model ) {
var item = this,
module = item.module,
_model = item_model || item(),
//Let's fall back on the id if the title is not set or empty
_title = ( _.has( _model, 'title') && ! _.isEmpty( _model.title ) ) ? api.CZR_Helpers.capitalize( _model.title ) : _model.id;
_title = api.CZR_Helpers.truncate( _title, 20 );
$( '.' + module.control.css_attr.item_title , item.container ).text( _title );
//add a hook here
api.CZR_Helpers.doActions('after_writeViewTitle', item.container , _model, item );
},
//@param : obj = { event : {}, model : {}, view : ${} }
//Fired on view_rendered:new when a new model has been added
//Fired on click on edit_view_btn
setViewVisibility : function( obj, is_added_by_user ) {
var item = this,
module = this.module;
if ( is_added_by_user ) {
item.viewState.set( 'expanded_noscroll' );
} else {
module.closeAllItems( item.id );
if ( _.has(module, 'preItem') ) {
module.preItemExpanded.set(false);
}
item.viewState.set( 'expanded' == item._getViewState() ? 'closed' : 'expanded' );
}
},
_getViewState : function() {
return -1 == this.viewState().indexOf('expanded') ? 'closed' : 'expanded';
},
// callback of item.viewState.callbacks
// viewState can take 3 states : expanded, expanded_noscroll, closed
toggleItemExpansion : function( status, duration ) {
var visible = 'closed' != status,
item = this,
module = this.module,
$el = $( '.' + module.control.css_attr.item_content , item.container ).first(),
dfd = $.Deferred(),
_slideComplete = function( visible ) {
item.container.toggleClass( 'open' , visible );
//close all remove dialogs
if ( visible )
module.closeRemoveDialogs();
//toggle the icon activate class depending on the status
//switch icon
var $_edit_icon = $el.siblings().find('.' + module.control.css_attr.edit_view_btn );
$_edit_icon.toggleClass('active' , visible );
if ( visible )
$_edit_icon.removeClass('fa-pencil-alt').addClass('fa-minus-square').attr('title', serverControlParams.i18n.close );
else
$_edit_icon.removeClass('fa-minus-square').addClass('fa-pencil-alt').attr('title', serverControlParams.i18n.edit );
//scroll to the currently expanded view
if ( 'expanded' == status ) {
module._adjustScrollExpandedBlock( item.container );
}
dfd.resolve();
};
duration = _.isUndefined( duration ) ? 150 : duration;
if ( visible ) {
$el.stop( true, true ).slideDown( duration, function() { _slideComplete( visible ); } );
} else {
$el.stop( true, true ).slideUp( 0, function() { _slideComplete( visible ); } );
}
return dfd.promise();
},
//removes the view dom module
_destroyView : function ( duration ) {
this.container.fadeOut( {
duration : duration ||400,
done : function() {
$(this).remove();
}
});
},
// LOADER HELPERS
// @return void()
// print a loader between the moment the item container is appended, and the item content is fetched from the server
printLoader : function() {
var item = this;
item.container
.css({'position' :'relative'})
.append( api.CZR_Helpers.css_loader_html ).find('.czr-css-loader').fadeIn( 'fast' );
// Start the countdown for auto-cleaning
clearTimeout( $.data( this, '_czr_loader_active_timer_') );
$.data( this, '_czr_loader_active_timer_', setTimeout(function() {
item.cleanLoader();
}, 5000 ) );
},
// @return void()
cleanLoader : function() {
this.container
.css({'min-height' : ''})
.find('.czr-css-loader').remove();
},
});//$.extend
})( wp.customize , jQuery, _ );//extends api.Value
//options:
// module : module,
// initial_modOpt_model : modOpt, can contains the already db saved values
// defaultModOptModel : module.defaultModOptModel
// control : control instance
var CZRModOptMths = CZRModOptMths || {};
( function ( api, $, _ ) {
$.extend( CZRModOptMths , {
initialize: function( options ) {
if ( _.isUndefined(options.module) || _.isEmpty(options.module) ) {
throw new Error('No module assigned to modOpt.');
}
var modOpt = this;
api.Value.prototype.initialize.call( modOpt, null, options );
//DEFERRED STATES
//store the state of ready.
//=> we don't want the ready method to be fired several times
modOpt.isReady = $.Deferred();
//VARIOUS DEFINITIONS
modOpt.container = null;//will store the modOpt $ dom element
modOpt.inputCollection = new api.Value({});
//input.options = options;
//write the options as properties, name is included
$.extend( modOpt, options || {} );
//declares a default modOpt model
modOpt.defaultModOptModel = _.clone( options.defaultModOptModel ) || { is_mod_opt : true };
//set initial values
var _initial_model = $.extend( modOpt.defaultModOptModel, options.initial_modOpt_model );
var ctrl = modOpt.module.control;
//this won't be listened to at this stage
modOpt.set( _initial_model );
//OPTIONS IS READY
//observe its changes when ready
modOpt.isReady.done( function() {
//listen to any modOpt change
//=> done in the module
//modOpt.callbacks.add( function() { return modOpt.modOptReact.apply(modOpt, arguments ); } );
//When shall we render the modOpt ?
//If the module is part of a simple control, the modOpt can be render now,
//modOpt.mayBeRenderModOptWrapper();
//RENDER THE CONTROL TITLE GEAR ICON
if( ! $( '.' + ctrl.css_attr.edit_modopt_icon, ctrl.container ).length ) {
$.when( ctrl.container
.find('.customize-control-title').first()//was.find('.customize-control-title')
.append( $( '', {
class : [ ctrl.css_attr.edit_modopt_icon, 'fas fa-cog' ].join(' '),
title : serverControlParams.i18n['Settings']
} ) ) )
.done( function(){
$( '.' + ctrl.css_attr.edit_modopt_icon, ctrl.container ).fadeIn( 400 );
});
}
//LISTEN TO USER ACTIONS ON CONTROL EL
api.CZR_Helpers.setupDOMListeners(
[
//toggle mod options
{
trigger : 'click keydown',
selector : '.' + ctrl.css_attr.edit_modopt_icon,
name : 'toggle_mod_option',
actions : function() {
// @see : moduleCtor::maybeAwakeAndBindSharedModOpt => api.czr_ModOptVisible.bind()
api.czr_ModOptVisible( ! api.czr_ModOptVisible(), {
module : modOpt.module,//the current module for which the modOpt is being expanded
focus : false//the id of the tab we want to focus on
});
}
}
],//actions to execute
{ dom_el: ctrl.container },//dom scope
modOpt //instance where to look for the cb methods
);
//modOpt.userEventMap = new api.Value( [] );
});//modOpt.isReady.done()
},//initialize
//overridable method
//Fired if the modOpt has been instantiated
//The modOpt.callbacks are declared.
ready : function() {
this.isReady.resolve();
}
});//$.extend
})( wp.customize , jQuery, _ );//extends api.CZRBaseControl
var CZRModOptMths = CZRModOptMths || {};
( function ( api, $, _ ) {
$.extend( CZRModOptMths , {
//fired when modOpt is ready and embedded
//define the modOpt view DOM event map
//bind actions when the modOpt is embedded
modOptWrapperViewSetup : function( modOpt_model ) {
var modOpt = this,
module = this.module,
dfd = $.Deferred(),
_setupDOMListeners = function( $_container ) {
//DOM listeners for the user action in modOpt view wrapper
api.CZR_Helpers.setupDOMListeners(
[
//toggle mod options
{
trigger : 'click keydown',
selector : '.' + module.control.css_attr.close_modopt_icon,
name : 'close_mod_option',
actions : function() {
// @see : moduleCtor::maybeAwakeAndBindSharedModOpt => api.czr_ModOptVisible.bind()
api.czr_ModOptVisible( false, {
module : module,//the current module for which the modOpt is being expanded
focus : false//the id of the tab we want to focus on
});
}
},
//tabs navigation
{
trigger : 'click keydown',
selector : '.tabs nav li',
name : 'tab_nav',
actions : function( args ) {
//toggleTabVisibility is declared in the module ctor and its "this" is the item or the modOpt
var tabIdSwitchedTo = $( args.dom_event.currentTarget, args.dom_el ).data('tab-id');
this.module.toggleTabVisibility.call( this, tabIdSwitchedTo );
this.trigger( 'tab-switch', { id : tabIdSwitchedTo } );
}
}
],//actions to execute
{ dom_el: $_container },//model + dom scope
modOpt //instance where to look for the cb methods
);
};
modOpt_model = modOpt() || modOpt.initial_modOpt_model;//could not be set yet
//renderview content now
modOpt.renderModOptContent( modOpt_model )
.done( function( $_container ) {
//update the $.Deferred state
if ( ! _.isEmpty( $_container ) && 0 < $_container.length ) {
_setupDOMListeners( $_container );
dfd.resolve( $_container );
}
else {
throw new Error( 'Module : ' + modOpt.module.id + ', the modOpt content has not been rendered' );
}
})
.fail( function( _r_ ) {
api.errorLog( "failed modOpt.renderModOptContent for module : " + module.id, _r_ );
})
.then( function() {
//the modOpt.container is now available
//Setup the tabs navigation
//setupTabNav is defined in the module ctor and its this is the item or the modOpt
modOpt.module.setupTabNav.call( modOpt );
});
return dfd.promise();
},
//renders saved modOpt views
//returns a promise( $container )
//the saved modOpt look like :
//array[ { id : 'sidebar-one', title : 'A Title One' }, {id : 'sidebar-two', title : 'A Title Two' }]
renderModOptContent : function( modOpt_model ) {
//=> an array of objects
var modOpt = this,
module = this.module,
dfd = $.Deferred();
modOpt_model = modOpt_model || modOpt();
var appendAndResolve = function( _tmpl_ ) {
//do we have an html template ?
if ( _.isEmpty( _tmpl_ ) ) {
dfd.reject( 'renderModOptContent => Missing html template for module : '+ module.id );
}
var _ctrlLabel = '';
try {
_ctrlLabel = [ serverControlParams.i18n['Options for'], module.control.params.label ].join(' ');
} catch( er ) {
api.errorLog( 'renderItemContent => Problem with ctrl label => ' + er );
_ctrlLabel = serverControlParams.i18n['Settings'];
}
$('#widgets-left').after( $( '', {
class : module.control.css_attr.mod_opt_wrapper,
html : [
[ '', _ctrlLabel , '
' ].join(''),
''
].join('')
} ) );
//render the mod opt content for this module
$( '.' + module.control.css_attr.mod_opt_wrapper ).append( _tmpl_ );
dfd.resolve( $( '.' + module.control.css_attr.mod_opt_wrapper ) );
};//appendAndResolve
// Do we have view content template script?
// if yes, let's use it <= Old way
// Otherwise let's fetch the html template from the server
if ( ! _.isEmpty( module.itemPreAddEl ) ) {
var tmplSelectorSuffix = module.getTemplateSelectorPart( 'modOptInputList', modOpt_model );
if ( 1 > $( '#tmpl-' + tmplSelectorSuffix ).length ) {
dfd.reject( 'renderModOptContent => No modOpt content template defined for module ' + module.id + '. The template script id should be : #tmpl-' + tmplSelectorSuffix );
}
appendAndResolve( wp.template( tmplSelectorSuffix )( modOpt_model ) );
} else {
api.CZR_Helpers.getModuleTmpl( {
tmpl : 'mod-opt',
module_type: module.module_type,
module_id : module.id,
control_id : module.control.id
} ).done( function( _serverTmpl_ ) {
//console.log( 'renderModOptContent => success response =>', _serverTmpl_);
appendAndResolve( api.CZR_Helpers.parseTemplate( _serverTmpl_ )( modOpt_model ) );
}).fail( function( _r_ ) {
//console.log( 'renderModOptContent => fail response =>', _r_);
dfd.reject( 'renderPreItemView => Problem when fetching the mod-opt tmpl from server for module : '+ module.id );
});
}
return dfd.promise();
},
toggleModPanelView : function( visible ) {
var modOpt = this,
module = this.module,
ctrl = module.control,
dfd = $.Deferred();
module.control.container.toggleClass( 'czr-modopt-visible', visible );
$('body').toggleClass('czr-editing-modopt', visible );
//Let the panel slide ( -webkit-transition: left .18s ease-in-out )
_.delay( function() {
dfd.resolve();
}, 200 );
return dfd.promise();
}
});//$.extend
})( wp.customize , jQuery, _ );//MULTI CONTROL CLASS
//extends api.Value
//
//Setup the collection of items
//renders the control view
//Listen to items collection changes and update the control setting
//MODULE OPTIONS :
// control : control,
// crud : bool
// id : '',
// items : [], module.items,
// modOpt : {}
// module_type : module.module_type,
// multi_item : bool
// section : module.section,
var CZRModuleMths = CZRModuleMths || {};
( function ( api, $, _ ) {
$.extend( CZRModuleMths, {
initialize: function( id, constructorOptions ) {
if ( _.isUndefined(constructorOptions.control) || _.isEmpty(constructorOptions.control) ) {
throw new Error('No control assigned to module ' + id );
}
var module = this;
api.Value.prototype.initialize.call( this, null, constructorOptions );
//store the state of ready.
//=> we don't want the ready method to be fired several times
module.isReady = $.Deferred();
//write the module constructor options as properties
// The default module model can be get with
// and is formed this way :
//@see getDefaultModuleApiModel : function() {
//if embedded in a control, amend the common model with the section id
// return {
// id : '',//module.id,
// module_type : '',//module.module_type,
// modOpt : {},//the module modOpt property, typically high level properties that area applied to all items of the module
// items : [],//$.extend( true, {}, module.items ),
// crud : false,
// hasPreItem : true,//a crud module has a pre item by default
// refresh_on_add_item : true,// the preview is refreshed on item add
// multi_item : false,
// sortable : false,//<= a module can be multi-item but not necessarily sortable
// control : {},//control,
// section : ''
// };
// },
$.extend( module, constructorOptions || {} );
//extend the module with new template Selectors
//can have been overriden at this stage from a module constructor
$.extend( module, {
crudModulePart : module.crudModulePart || '', //'czr-crud-module-part',//create, read, update, delete
rudItemPart : module.rudItemPart || '',// 'czr-rud-item-part',//read, update, delete
ruItemPart : module.ruItemPart || '',// 'czr-ru-item-part',//read, update <= ONLY USED IN THE WIDGET MODULE
alertPart : module.alertPart || '',// 'czr-rud-item-alert-part',//used both for items and modules removal
itemInputList : module.itemInputList || '',//is specific for each crud module
modOptInputList : module.modOptInputList || ''//is specific for each module
} );
//embed : define a container, store the embed state, fire the render method
module.embedded = $.Deferred();
module.itemsWrapper = '';//will store the $ item container
//if a module is embedded in a control, its container == the control container.
module.container = $( module.control.selector );
//render the item(s) wrapper
//and resolve the module.embedded promise()
module.renderModuleParts()
.done( function( $_module_items_wrapper ){
if ( false === $_module_items_wrapper.length ) {
throw new Error( 'The items wrapper has not been rendered for module : ' + module.id );
}
//stores the items wrapper ( el ) as a jQuery var
module.itemsWrapper = $_module_items_wrapper;
module.embedded.resolve();
})
.fail( function( _r_ ) {
throw new Error( [ "initialize module => failed module.renderModuleParts() for module : " , module.id , _r_ ].join(' '));
});
/*-----------------------------------------------
* MODULE OPTIONS
------------------------------------------------*/
//declares a default Mod options API model
module.defaultAPImodOptModel = {
initial_modOpt_model : {},
defaultModOptModel : {},
control : {},//control instance
module : {}//module instance
};
//declares a default modOpt model
module.defaultModOptModel = {};
//define a default Constructors
module.modOptConstructor = module.modOptConstructor || api.CZRModOpt;
/*-----------------------------------------------
* ITEMS
------------------------------------------------*/
module.itemCollection = new api.Value( [] );
//declares a default Item API model
module.defaultAPIitemModel = {
id : '',
initial_item_model : {},
defaultItemModel : {},
control : {},//control instance
module : {},//module instance
is_added_by_user : false
};
// declares a default item model
module.defaultItemModel = api.czrModuleMap[ module.module_type ].defaultItemModel || { id : '', title : '' };
// item constuctor : use the constructor already defined in a module, or fallback on the default one
module.itemConstructor = module.itemConstructor || api.CZRItem;
// czr_model stores the each model value => one value by created by model.id
module.czr_Item = new api.Values();
/*-----------------------------------------------
* SET THE DEFAULT INPUT CONSTRUCTOR AND INPUT OPTIONS
------------------------------------------------*/
// input constuctor : use the constructor already defined in a module, or fallback on the default one
module.inputConstructor = module.inputConstructor || api.CZRInput;//constructor for the items input
if ( module.hasModOpt() ) {
//use the constructor already defined in a module, or fallback on the default one
module.inputModOptConstructor = module.inputModOptConstructor || api.CZRInput;//constructor for the modOpt input
}
module.inputOptions = {};//<= can be set by each module specifically
//For example, if I need specific options for the content_picker, this is where I will set them in the module extended object
/*-----------------------------------------------
* FIRE ON isReady
------------------------------------------------*/
//module.ready(); => fired by children
module.isReady.done( function() {
//store the module dirtyness, => no items set
module.isDirty = new api.Value( constructorOptions.dirty || false );
//initialize the module api.Value()
//constructorOptions has the same structure as the one described in prepareModuleforAPI
//setting the module Value won't be listen to at this stage
//api.infoLog('module.isReady.done() => constructorOptions', constructorOptions);
module.initializeModuleModel( constructorOptions )
.done( function( initialModuleValue ) {
module.set( initialModuleValue );
})
.fail( function( response ){ api.errare( 'Module : ' + module.id + ' initialize module model failed : ', response ); })
.always( function( initialModuleValue ) {
//listen to each single module change
module.callbacks.add( function() { return module.moduleReact.apply( module, arguments ); } );
//if the module is not registered yet (for example when the module is added by user),
//=> push it to the collection of the module-collection control
//=> updates the wp api setting
if ( ! module.control.isModuleRegistered( module.id ) ) {
module.control.updateModulesCollection( { module : constructorOptions, is_registered : false } );
}
module.bind('items-collection-populated', function( collection ) {
//listen to item Collection changes
module.itemCollection.callbacks.add( function() { return module.itemCollectionReact.apply( module, arguments ); } );
// The sortable property is set on module registration
// if not specified, the sortable will be set to true by default if the module is crud or multi-item
if ( false !== module.sortable ) {
module._makeItemsSortable();
}
// this event is listened to by Nimble Builder to expand the module once all the items collection is populated
module.control.container.trigger('items-collection-populated');
});
//populate and instantiate the items now when a module is embedded in a regular control
module.populateSavedItemCollection();
//When the module has modOpt :
//=> Instantiate the modOpt and setup listener
if ( module.hasModOpt() ) {
module.instantiateModOpt();
}
});
});//module.isReady.done()
/*-----------------------------------------------
* Maybe resolve isReady() on parent section expanded
------------------------------------------------*/
if ( true === api.czrModuleMap[ module.module_type ].ready_on_section_expanded ) {
//fired ready :
//1) on section expansion
//2) or in the case of a module embedded in a regular control, if the module section is already opened => typically when skope is enabled
if ( _.has( api, 'czr_activeSectionId' ) && module.control.section() == api.czr_activeSectionId() && 'resolved' != module.isReady.state() ) {
module.embedded.then( function() {
module.ready();
});
}
// defer the expanded callback when the section is instantiated
api.section( module.control.section(), function( _section_ ) {
_section_.expanded.bind(function(to) {
//set module ready on section expansion
if ( 'resolved' != module.isReady.state() ) {
module.embedded.then( function() {
module.ready();
});
}
});
});
}
/*-----------------------------------------------
* Maybe resolve isReady() on custom control event
// To be specified when registering the control
// used in Nimble to delay the instantiation of the input when the control accordion is expanded
------------------------------------------------*/
var _control_event = api.czrModuleMap[ module.module_type ].ready_on_control_event;
if ( ! _.isUndefined( _control_event ) ) {
// defer the expanded callback when the section is instantiated
api.control( module.control.id, function( _control_ ) {
_control_.container.on( _control_event, function(evt) {
//set module ready on module accordion expansion
if ( 'resolved' != module.isReady.state() ) {
module.embedded.then( function() {
module.ready();
});
}
});
});
}
// Maybe instantiate and bind the api.Value() controlling the module option panel, for the module using it ( has_mod_opt : true on registration )
this.maybeAwakeAndBindSharedModOpt();
},//initialize()
//////////////////////////////////
///READY
//////////////////////////////////
//When the control is embedded on the page, this method is fired in api.CZRBaseModuleControl:ready()
//=> right after the module is instantiated.
//If the module is a dynamic one (CRUD like), then this method is invoked by the child class
ready : function() {
var module = this;
module.isReady.resolve();
},
//fired when module is initialized, on module.isReady.done()
//designed to be extended or overridden to add specific items or properties
initializeModuleModel : function( constructorOptions ) {
var module = this, dfd = $.Deferred();
if ( ! module.isMultiItem() && ! module.isCrud() ) {
//this is a static module. We only have one item
//init module item if needed.
if ( _.isEmpty( constructorOptions.items ) ) {
var def = _.clone( module.defaultItemModel );
constructorOptions.items = [ $.extend( def, { id : module.id } ) ];
}
}
return dfd.resolve( constructorOptions ).promise();
},
//cb of : module.itemCollection.callbacks
//the data can typically hold informations passed by the input that has been changed and its specific preview transport (can be PostMessage )
//data looks like :
//{
// module : {}
// input_changed : string input.id
// input_transport : 'postMessage' or '',
// not_preview_sent : bool
//}
itemCollectionReact : function( to, from, data ) {
var module = this,
_current_model = module(),
_new_model = $.extend( true, {}, _current_model );
_new_model.items = to;
//update the dirtyness state
module.isDirty.set(true);
//set the the new items model
module.set( _new_model, data || {} );
},
//This method is fired from the control
filterItemsBeforeCoreApiSettingValue : function( itemsToReturn ) {
return itemsToReturn;
},
//cb of module.callbacks
//=> sets the setting value via the module collection !
moduleReact : function( to, from, data ) {
//cb of : module.callbacks
var module = this,
control = module.control,
isItemUpdate = ( _.size( from.items ) == _.size( to.items ) ) && ! _.isEmpty( _.difference( to.items, from.items ) ),
isColumnUpdate = to.column_id != from.column_id,
refreshPreview = function() {
api.previewer.refresh();
};
//update the collection + pass data
control.updateModulesCollection( {
module : $.extend( true, {}, to ),
data : data//useful to pass contextual info when a change happens
} );
// //Always update the view title
// module.writeViewTitle(to);
// //@todo : do we need that ?
// //send module to the preview. On update only, not on creation.
// if ( ! _.isEmpty(from) || ! _.isUndefined(from) ) {
// module._sendModule(to, from);
// }
},
//@todo : create a smart helper to get either the wp api section or the czr api sektion, depending on the module context
getModuleSection : function() {
return this.section;
},
//is this module multi item ?
//@return bool
isMultiItem : function() {
return api.CZR_Helpers.isMultiItemModule( null, this );
},
//is this module crud ?
//@return bool
isCrud : function() {
return api.CZR_Helpers.isCrudModule( null, this );
},
hasModOpt : function() {
return api.CZR_Helpers.hasModuleModOpt( null, this );
},
//////////////////////////////////
///MODULE OPTION :
///1) PREPARE
///2) INSTANTIATE
///3) LISTEN TO AND SET PARENT MODULE ON CHANGE
//////////////////////////////////
//fired when module isReady
instantiateModOpt : function() {
var module = this;
//Prepare the modOpt and instantiate it
var modOpt_candidate = module.prepareModOptForAPI( module().modOpt || {} );
module.czr_ModOpt = new module.modOptConstructor( modOpt_candidate );
module.czr_ModOpt.ready();
//update the module model on modOpt change
module.czr_ModOpt.callbacks.add( function( to, from, data ) {
var _current_model = module(),
_new_model = $.extend( true, {}, _current_model );
_new_model.modOpt = to;
//update the dirtyness state
module.isDirty(true);
//set the the new items model
//the data can typically hold informations passed by the input that has been changed and its specific preview transport (can be PostMessage )
//data looks like :
//{
// module : {}
// input_changed : string input.id
// input_transport : 'postMessage' or '',
// not_preview_sent : bool
//}
module( _new_model, data );
});
},
//@return an API ready modOpt object with the following properties
// initial_modOpt_model : {},
// defaultModOptModel : {},
// control : {},//control instance
// module : {},//module instance
//@param modOpt_candidate is an object. Can contain the saved modOpt properties on init.
prepareModOptForAPI : function( modOpt_candidate ) {
var module = this,
api_ready_modOpt = {};
// if ( ! _.isObject( modOpt_candidate ) ) {
// throw new Error('preparemodOptForAPI : a modOpt must be an object to be instantiated.');
// }
modOpt_candidate = _.isObject( modOpt_candidate ) ? modOpt_candidate : {};
_.each( module.defaultAPImodOptModel, function( _value, _key ) {
var _candidate_val = modOpt_candidate[_key];
switch( _key ) {
case 'initial_modOpt_model' :
//make sure that the provided modOpt has all the default properties set
_.each( module.getDefaultModOptModel() , function( _value, _property ) {
if ( ! _.has( modOpt_candidate, _property) )
modOpt_candidate[_property] = _value;
});
api_ready_modOpt[_key] = modOpt_candidate;
break;
case 'defaultModOptModel' :
api_ready_modOpt[_key] = _.clone( module.defaultModOptModel );
break;
case 'control' :
api_ready_modOpt[_key] = module.control;
break;
case 'module' :
api_ready_modOpt[_key] = module;
break;
}//switch
});
return api_ready_modOpt;
},
//Returns the default modOpt defined in initialize
//Each chid class can override the default item and the following method
getDefaultModOptModel : function( id ) {
var module = this;
return $.extend( _.clone( module.defaultModOptModel ), { is_mod_opt : true } );
},
//The idea is to send only the currently modified item instead of the entire collection
//the entire collection is sent anyway on api(setId).set( value ), and accessible in the preview via api(setId).bind( fn( to) )
//This method can be called on input change and on czr-partial-refresh-done
//{
// input_id :
// input_parent_id :
// is_mod_opt :
// to :
// from :
// isPartialRefresh : bool//<= let us know if it is a full wrapper refresh or a single input update ( true when fired from sendModuleInputsToPreview )
//}
sendInputToPreview : function( args ) {
var module = this;
//normalizes the args
args = _.extend(
{
input_id : '',
input_parent_id : '',//<= can be the mod opt or an item
to : null,
from : null
} , args );
if ( _.isEqual( args.to, args.from ) )
return;
//This is listened to by the preview frame
api.previewer.send( 'czr_input', {
set_id : api.CZR_Helpers.getControlSettingId( module.control.id ),
module_id : module.id,//<= will allow us to target the right dom element on front end
module : { items : $.extend( true, {}, module().items ) , modOpt : module.hasModOpt() ? $.extend( true, {}, module().modOpt ): {} },
input_parent_id : args.input_parent_id,//<= can be the mod opt or the item
input_id : args.input_id,
value : args.to,
isPartialRefresh : args.isPartialRefresh//<= let us know if it is a full wrapper refresh or a single input update ( true when fired from sendModuleInputsToPreview )
});
//add a hook here
module.trigger( 'input_sent', { input : args.to , dom_el: module.container } );
},
//@return void()
//Fired on partial refresh in base control initialize, only for module type controls
//This method can be called when don't have input instances available
//=> typically when reordering items, mod options and items are closed, therefore there's no input instances.
//=> the input id are being retrieved from the input parent models : items and mod options.
//@param args = { isPartialRefresh : bool }
sendModuleInputsToPreview : function( args ) {
var module = this,
_sendInputData = function() {
var inputParent = this,//this is the input parent : item or modOpt
inputParentModel = $.extend( true, {}, inputParent() );
//we don't need to send the id, which is never an input, but generated by the api.
inputParentModel = _.omit( inputParentModel, 'id' );
_.each( inputParentModel, function( inputVal, inputId ) {
module.sendInputToPreview( {
input_id : inputId,
input_parent_id : inputParent.id,
to : inputVal,
from : null,
isPartialRefresh : args.isPartialRefresh
});
});
};
module.czr_Item.each( function( _itm_ ) {
_sendInputData.call( _itm_ );
});
if ( module.hasModOpt() ) {
_sendInputData.call( module.czr_ModOpt );
}
},
// Fired in ::initialize()
// Maybe instantiate and bind the api.Value() controlling the visibility of the module option panel, for the module using it ( has_mod_opt : true on registration )
maybeAwakeAndBindSharedModOpt : function() {
if ( ! _.isUndefined( api.czr_ModOptVisible ) )
return;
//MOD OPT PANEL SETTINGS
api.czr_ModOptVisible = new api.Value( false );
//MOD OPT VISIBLE REACT
// passing an optional args object allows us to expand the modopt panel and focus on a specific tab right after
//@args : {
// module : module,//the current module for which the modOpt is being expanded
// focus : 'section-topline-2'//the id of the tab we want to focus on
//}
api.czr_ModOptVisible.bind( function( visible, from, args ) {
args = args || {};
if ( ! _.isFunction( args.module ) || ! _.isFunction( args.module.czr_ModOpt ) ) {
api.errare( 'moduleCtor::maybeAwakeAndBindSharedModOpt => api.czr_ModOptVisible.bind() => incorrect arguments', args );
return;
}
var modOpt = args.module.czr_ModOpt,
module = args.module;
// Append content on expansion
// Remove on collapse
if ( visible ) {
//first close all opened remove dialogs and opened items
module.closeRemoveDialogs().closeAllItems();
modOpt.modOptWrapperViewSetup( modOpt() ).done( function( $_container ) {
modOpt.container = $_container;
try {
api.CZR_Helpers.setupInputCollectionFromDOM.call( modOpt ).toggleModPanelView( visible );
} catch(e) {
api.consoleLog(e);
}
if ( module && args.focus ) {
_.delay( function() {
if ( _.isNull( modOpt.container ) || ! modOpt.container.find('[data-tab-id="' + args.focus + '"] a').length )
return;
modOpt.container.find('[data-tab-id="' + args.focus + '"] a').trigger('click');
}, 200 );
}
});
} else {
modOpt.toggleModPanelView( visible ).done( function() {
if ( modOpt.container && 0 < modOpt.container.length ) {
$.when( modOpt.container.remove() ).done( function() {
api.CZR_Helpers.removeInputCollection.call( modOpt );
});
} else {
api.CZR_Helpers.removeInputCollection.call( modOpt );
}
modOpt.container = null;
});
}
} );
}
});//$.extend//CZRBaseControlMths
})( wp.customize , jQuery, _ );//MULTI CONTROL CLASS
//extends api.CZRBaseControl
//
//Setup the collection of items
//renders the module view
//Listen to items collection changes and update the control setting
var CZRModuleMths = CZRModuleMths || {};
( function ( api, $, _ ) {
$.extend( CZRModuleMths, {
//@fired in module ready on api('ready')
//the module().items has been set in initialize
//A collection of items can be supplied.
populateSavedItemCollection : function( _itemCollection_ ) {
var module = this,
_deepCopyOfItemCollection;
if ( ! _.isArray( _itemCollection_ || module().items ) ) {
api.errorLog( 'populateSavedItemCollection : The saved items collection must be an array in module :' + module.id );
return;
}
_deepCopyOfItemCollection = $.extend( true, [], _itemCollection_ || module().items );
//populates the collection with the saved items
//the modOpt must be skipped
//the saved items + modOpt is an array looking like :
////MODOPT IS THE FIRST ARRAY ELEMENT: A modOpt has no unique id and has the property is_mod_opt set to true
//[
// is_mod_opt : true //<= inform us that this is not an item but a modOpt
//],
////THEN COME THE ITEMS
//[
// id : "czr_slide_module_0"
// slide-background : 21,
// ....
// ],
// [
// id : "czr_slide_module_1"
// slide-background : 21,
// ....
// ]
// CHECK THAT WE DON'T HAVE ANY MODOPT AT THIS STAGE
//=> the items and the modOpt should already be split at this stage, because it's done before module instantiation... this check is totally paranoid.
_.each( _deepCopyOfItemCollection , function( item_candidate , key ) {
if ( _.has( item_candidate, 'is_mod_opt' ) ) {
throw new Error( 'populateSavedItemCollection => there should be no mod opt to instantiate here.');
}
});
// allow modules to hook here
module.trigger( 'filterItemCandidatesBeforeInstantiation', _deepCopyOfItemCollection );
//INSTANTIATE THE ITEMS
_.each( _deepCopyOfItemCollection, function( item_candidate , key ) {
//instantiates and fires ready
var _doInstantiate_ = function() {
var _item_instance_ = module.instantiateItem( item_candidate );
if ( _.isFunction( _item_instance_ ) ) {
_item_instance_.ready();
} else {
api.errare( 'populateSavedItemCollection => Could not instantiate item in module ' + module.id , item_candidate );
}
};
//adds it to the collection and fire item.ready()
if ( serverControlParams.isDevMode ) {
_doInstantiate_();
} else {
try { _doInstantiate_(); } catch( er ) {
api.errare( 'populateSavedItemCollection => ' + er );
}
}
});
//check if everything went well
_.each( _deepCopyOfItemCollection, function( _item ) {
if ( ! _.isObject( _item ) ) {
return;
}
if ( _.isUndefined( _.findWhere( module.itemCollection(), _item.id ) ) ) {
throw new Error( 'populateSavedItemCollection => The saved items have not been properly populated in module : ' + module.id );
}
});
module.trigger( 'items-collection-populated' );
//do we need to chain this method ?
//return this;
},
instantiateItem : function( item_candidate, is_added_by_user ) {
var module = this;
// Cast to an object now.
item_candidate = _.isObject( item_candidate ) ? item_candidate : {};
// FIRST VALIDATION
//allow modules to validate the item_candidate before addition
item_candidate = module.validateItemBeforeAddition( item_candidate, is_added_by_user );
// Abort here and display a simple console message if item is null or false, for example if validateItemBeforeAddition returned null or false
if ( ! item_candidate || _.isNull( item_candidate ) ) {
api.errare( 'CZRModule::instantiateItem() => item_candidate did not pass validation in module ' + module.id );
return;
}
// NORMALIZE
//Prepare the item, make sure its id is set and unique
item_candidate = module.prepareItemForAPI( item_candidate );
if ( ! _.isObject( item_candidate ) ) {
api.errare( 'CZRModule::instantiateItem() => an item should be described by an object in module type : ' + module.module_type, 'module id : ' + module.id );
return;
}
// Display a simple console message if item is null or false, for example if validateItemBeforeInstantiation returned null or false
if ( ! item_candidate || _.isNull( item_candidate ) ) {
api.errare( 'CZRModule::instantiateItem() => item_candidate invalid in module ' + module.id );
return;
}
//ITEM ID CHECKS
if ( ! _.has( item_candidate, 'id' ) ) {
throw new Error('CZRModule::instantiateItem() => an item has no id and could not be added in the collection of : ' + this.id );
}
if ( module.czr_Item.has( item_candidate.id ) ) {
throw new Error('CZRModule::instantiateItem() => the following item id ' + item_candidate.id + ' already exists in module.czr_Item() for module ' + this.id );
}
//instantiate the item with the item constructor, default one or provided by the module
module.czr_Item.add( item_candidate.id, new module.itemConstructor( item_candidate.id, item_candidate ) );
if ( ! module.czr_Item.has( item_candidate.id ) ) {
throw new Error('CZRModule::instantiateItem() => instantiation failed for item id ' + item_candidate.id + ' for module ' + this.id );
}
//the item is now ready and will listen to changes
//return the instance
return module.czr_Item( item_candidate.id );
},
// Designed to be overriden in modules
validateItemBeforeAddition : function( item_candidate, is_added_by_user ) {
return item_candidate;
},
//@return an API ready item object with the following properties
// id : '',
// initial_item_model : {},
// defaultItemModel : {},
// control : {},//control instance
// module : {},//module instance
// is_added_by_user : false
prepareItemForAPI : function( item_candidate ) {
var module = this,
api_ready_item = {};
// if ( ! _.isObject( item_candidate ) ) {
// throw new Error('prepareitemForAPI : a item must be an object to be instantiated.');
// }
item_candidate = _.isObject( item_candidate ) ? item_candidate : {};
_.each( module.defaultAPIitemModel, function( _value, _key ) {
var _candidate_val = item_candidate[_key];
switch( _key ) {
case 'id' :
// The id can be specified in a module ( ex: the pre defined item ids of the Font Customizer module )
// => that's why we need to check here if the item id is not already registered here
if ( _.isEmpty( _candidate_val ) ) {
api_ready_item[_key] = module.generateItemId( module.module_type );
} else {
if ( module.isItemRegistered( _candidate_val ) ) {
module.generateItemId( _candidate_val );
} else {
api_ready_item[_key] = _candidate_val;
}
}
break;
case 'initial_item_model' :
//make sure that the provided item has all the default properties set
_.each( module.getDefaultItemModel() , function( _value, _property ) {
if ( ! _.has( item_candidate, _property) )
item_candidate[_property] = _value;
});
api_ready_item[_key] = item_candidate;
break;
case 'defaultItemModel' :
api_ready_item[_key] = _.clone( module.defaultItemModel );
break;
case 'control' :
api_ready_item[_key] = module.control;
break;
case 'module' :
api_ready_item[_key] = module;
break;
case 'is_added_by_user' :
api_ready_item[_key] = _.isBoolean( _candidate_val ) ? _candidate_val : false;
break;
}//switch
});
//if we don't have an id at this stage, let's generate it.
if ( ! _.has( api_ready_item, 'id' ) ) {
api_ready_item.id = module.generateItemId( module.module_type );
}
//Now amend the initial_item_model with the generated id
api_ready_item.initial_item_model.id = api_ready_item.id;
return module.validateItemBeforeInstantiation( api_ready_item );
},
// Designed to be overriden in modules
validateItemBeforeInstantiation : function( api_ready_item ) {
return api_ready_item;
},
// recursive
// will generate a unique id with the provided prefix
generateItemId : function( prefix, key, i ) {
//prevent a potential infinite loop
i = i || 1;
if ( i > 100 ) {
throw new Error( 'Infinite loop when generating of a module id.' );
}
var module = this;
key = key || module._getNextItemKeyInCollection();
var id_candidate = prefix + '_' + key;
//do we have a module collection value ?
if ( ! _.has( module, 'itemCollection' ) || ! _.isArray( module.itemCollection() ) ) {
throw new Error('The item collection does not exist or is not properly set in module : ' + module.id );
}
//make sure the module is not already instantiated
if ( module.isItemRegistered( id_candidate ) ) {
key++; i++;
return module.generateItemId( prefix, key, i );
}
return id_candidate;
},
//helper : return an int
//=> the next available id of the item collection
_getNextItemKeyInCollection : function() {
var module = this,
_maxItem = {},
_next_key = 0;
//get the initial key
//=> if we already have a collection, extract all keys, select the max and increment it.
//else, key is 0
if ( _.isEmpty( module.itemCollection() ) )
return _next_key;
if ( _.isArray( module.itemCollection() ) && 1 === _.size( module.itemCollection() ) ) {
_maxItem = module.itemCollection()[0];
} else {
_maxItem = _.max( module.itemCollection(), function( _item ) {
if ( ! _.isNumber( _item.id.replace(/[^\/\d]/g,'') ) )
return 0;
return parseInt( _item.id.replace( /[^\/\d]/g, '' ), 10 );
});
}
//For a single item collection, with an index free id, it might happen that the item is not parsable. Make sure it is. Otherwise, use the default key 0
if ( ! _.isUndefined( _maxItem ) && _.isNumber( _maxItem.id.replace(/[^\/\d]/g,'') ) ) {
_next_key = parseInt( _maxItem.id.replace(/[^\/\d]/g,''), 10 ) + 1;
}
return _next_key;
},
//this helper allows to check if an item has been registered in the collection
//no matter if it's not instantiated yet
isItemRegistered : function( id_candidate ) {
var module = this;
return ! _.isUndefined( _.findWhere( module.itemCollection(), { id : id_candidate}) );
},
//Fired in module.czr_Item.itemReact
//@param args can be
//{
// collection : [],
// params : params {}
//},
//
//or {
// item : {}
// params : params {}
//}
//if a collection is provided in the passed args then simply refresh the collection
//=> typically used when reordering the collection item with sortable or when a item is removed
//
//the args.params can typically hold informations passed by the input that has been changed and its specific preview transport (can be PostMessage )
//params looks like :
//{
// module : {}
// input_changed : string input.id
// input_transport : 'postMessage' or '',
// not_preview_sent : bool
//}
//@return a deferred promise
updateItemsCollection : function( args ) {
var module = this,
_current_collection = module.itemCollection(),
_new_collection = _.clone(_current_collection),
dfd = $.Deferred();
//if a collection is provided in the passed args then simply refresh the collection
//=> typically used when reordering the collection item with sortable or when a item is removed
if ( _.has( args, 'collection' ) ) {
//reset the collection
module.itemCollection.set( args.collection );
return;
}
if ( ! _.has( args, 'item' ) ) {
throw new Error('updateItemsCollection, no item provided ' + module.control.id + '. Aborting');
}
//normalizes with params
args = _.extend( { params : {} }, args );
var item_candidate = _.clone( args.item ),
hasMissingProperty = false;
// Is the item well formed ? Does it have all the properties of the default model ?
// Each module has to declare a defaultItemModel which augments the default one : { id : '', title : '' };
// Let's loop on the defaultItemModel property and check that none is missing in the candidate
_.each( module.defaultItemModel, function( itemData, key ) {
if ( ! _.has( item_candidate, key ) ) {
throw new Error( 'CZRModuleMths => updateItemsCollection : Missing property "' + key + '" for item candidate' );
}
});
if ( hasMissingProperty )
return;
//the item already exist in the collection
if ( _.findWhere( _new_collection, { id : item_candidate.id } ) ) {
_.each( _current_collection , function( _item, _ind ) {
if ( _item.id != item_candidate.id )
return;
//set the new val to the changed property
_new_collection[_ind] = item_candidate;
});
}
//the item has to be added
else {
_new_collection.push( item_candidate );
}
//updates the collection value
//=> is listened to by module.itemCollectionReact
module.itemCollection.set( _new_collection, args.params );
return dfd.resolve( { collection : _new_collection, params : args.params } ).promise();
},
//fire on sortable() update callback
//@returns a sorted collection as an array of item objects
_getSortedDOMItemCollection : function( ) {
var module = this,
_old_collection = _.clone( module.itemCollection() ),
_new_collection = [],
dfd = $.Deferred();
//re-build the collection from the DOM
$( '.' + module.control.css_attr.single_item, module.container ).each( function( _index ) {
var _item = _.findWhere( _old_collection, {id: $(this).attr('data-id') });
//do we have a match in the existing collection ?
if ( ! _item )
return;
_new_collection[_index] = _item;
});
if ( _old_collection.length != _new_collection.length ) {
throw new Error('There was a problem when re-building the item collection from the DOM in module : ' + module.id );
}
return dfd.resolve( _new_collection ).promise();
},
//This method should
//1) remove the item views
//2) remove the czr_items instances
//3) remove the item collection
//4) re-initialize items
//5) re-setup the item collection
//6) re-instantiate the items
//7) re-render their views
refreshItemCollection : function() {
var module = this;
//Remove item views and instances
module.czr_Item.each( function( _itm ) {
if ( module.czr_Item( _itm.id ).container && 0 < module.czr_Item( _itm.id ).container.length ) {
$.when( module.czr_Item( _itm.id ).container.remove() ).done( function() {
//Remove item instances
module.czr_Item.remove( _itm.id );
});
}
});
// Reset the item collection
// => the collection listeners will be setup after populate, on 'items-collection-populated'
module.itemCollection = new api.Value( [] );
module.populateSavedItemCollection();
}
});//$.extend//CZRBaseControlMths
})( wp.customize , jQuery, _ );//MULTI CONTROL CLASS
//extends api.CZRBaseControl
//
//Setup the collection of items
//renders the module view
//Listen to items collection changes and update the control setting
var CZRModuleMths = CZRModuleMths || {};
( function ( api, $, _ ) {
$.extend( CZRModuleMths, {
//Returns the default item defined in initialize
//Each chid class can override the default item and the following method
getDefaultItemModel : function( id ) {
var module = this;
return $.extend( _.clone( module.defaultItemModel ), { id : id || '' } );
},
//////////////////////////////////
///MODEL HELPERS
//////////////////////////////////
//the job of this function is to return a new item ready to be added to the collection
//the new item shall have a unique id
//!!recursive
_initNewItem : function( _item , _next_key ) {
var module = this,
_new_item = { id : '' },
_id;
//get the next available key of the collection
_next_key = 'undefined' != typeof(_next_key) ? _next_key : _.size( module.itemCollection() );
if ( _.isNumber(_next_key) ) {
_id = module.module_type + '_' + _next_key;
}
else {
_id = _next_key;
//reset next key to 0 in case a recursive loop is needed later
_next_key = 0;
}
if ( _item && ! _.isEmpty( _item) )
_new_item = $.extend( _item, { id : _id } );
else
_new_item = this.getDefaultItemModel( _id );
//check the id existence, and its unicity
if ( _.has(_new_item, 'id') && module._isItemIdPossible(_id) ) {
//make sure that the provided item has all the default properties set
_.map( module.getDefaultItemModel() , function( value, property ){
if ( ! _.has(_new_item, property) )
_new_item[property] = value;
});
return _new_item;
}
//if id already exists, then test a new one
return module._initNewItem( _new_item, _next_key + 1);
}
});//$.extend
})( wp.customize , jQuery, _ );//MULTI CONTROL CLASS
//extends api.CZRBaseControl
//
//Setup the collection of items
//renders the module view
//Listen to items collection changes and update the control setting
var CZRModuleMths = CZRModuleMths || {};
( function ( api, $, _ ) {
$.extend( CZRModuleMths, {
//fired on module.isReady.done()
//the module.container is set.
renderModuleParts : function() {
var module = this,
$_moduleContentEl = $( module.container ),
dfd = $.Deferred();
var appendAndResolve = function( _tmpl_ ) {
if ( module.isCrud() ) {
//do we have an html template ?
if ( _.isEmpty( _tmpl_ ) ) {
dfd.reject( 'renderModuleParts => Missing html template for module : '+ module.id );
}
//append the module wrapper to the column
$_moduleContentEl.append( _tmpl_ );
}
// Always append this
var $_module_items_wrapper = $(
'',
{
class : [
module.control.css_attr.items_wrapper,
module.module_type,
module.isMultiItem() ? 'multi-item-mod' : 'mono-item-mod',
module.isCrud() ? 'crud-mod' : 'not-crud-mod'
].join(' ')
}
);
$_moduleContentEl.append( $_module_items_wrapper );
dfd.resolve( $( $_module_items_wrapper, $_moduleContentEl ) );
};//appendAndResolve
//Crud modules => then let's add the crud module part tmpl
if ( module.isCrud() ) {
// Do we have view content template script?
// if yes, let's use it <= Old way
// Otherwise let's fetch the html template from the server
if ( ! _.isEmpty( module.crudModulePart ) ) {
if ( 1 > $( '#tmpl-' + module.crudModulePart ).length ) {
dfd.reject( 'renderModuleParts => no crud Module Part template for module ' + module.id + '. The template script id should be : #tmpl-' + module.crudModulePart );
}
appendAndResolve( wp.template( module.crudModulePart )( {} ) );
} else {
api.CZR_Helpers.getModuleTmpl( {
tmpl : 'crud-module-part',
module_type: 'all_modules',
module_id : module.id,
control_id : module.control.id
} ).done( function( _serverTmpl_ ) {
//console.log( 'renderModuleParts => success response =>', module.id, _serverTmpl_);
appendAndResolve( api.CZR_Helpers.parseTemplate( _serverTmpl_ )( {} ) );
}).fail( function( _r_ ) {
api.errare( 'renderModuleParts => fail response =>', _r_);
dfd.reject( 'renderModuleParts => Problem when fetching the crud-module-part tmpl from server for module : '+ module.id );
});
}
} else {
appendAndResolve();
}
return dfd.promise();
},
//called before rendering a view. Fired in module::renderItemWrapper()
//can be overridden to set a specific view template depending on the model properties
//@return string
//@type can be
//Read Update Delete (rud...)
//Read Update (ru)
//...
//@item_model is an object describing the current item model
getTemplateSelectorPart : function( type, item_model ) {
var module = this, _el;
switch( type ) {
case 'rudItemPart' :
_el = module.rudItemPart;
break;
case 'ruItemPart' :
_el = module.ruItemPart;
break;
case 'modOptInputList' :
_el = module.modOptInputList;
break;
case 'itemInputList' :
_el = _.isFunction( module.itemInputList ) ? module.itemInputList( item_model ) : module.itemInputList;
break;
}
if ( _.isEmpty(_el) ) {
throw new Error('No valid template has been found in getTemplateSelectorPart() ' + module.id + '. Aborting');
} else {
return _el;
}
},
//helper
//get the $ view DOM el from the item id
getViewEl : function( id ) {
var module = this;
return $( '[data-id = "' + id + '"]', module.container );
},
//fired on add_item
//fired on views_sorted
closeAllItems : function( id ) {
var module = this,
_current_collection = _.clone( module.itemCollection() ),
_filtered_collection = _.filter( _current_collection , function( mod) { return mod.id != id; } );
_.each( _filtered_collection, function( _item ) {
if ( module.czr_Item.has(_item.id) && 'expanded' == module.czr_Item(_item.id)._getViewState(_item.id) )
module.czr_Item( _item.id ).viewState.set( 'closed' ); // => will fire the cb toggleItemExpansion
} );
// 'czr-all-items-closed' has been introduced when coding the Simple Nimble slider module. @see https://github.com/presscustomizr/nimble-builder/issues/82
// When using the text editor in the items of in a multi-item module
// We need to clear the editor instances each time all items are closed, before opening a new one
api.trigger('czr-all-items-closed', { module_id : module.id } );
return this;
},
//make sure a given jQuery block is fully visible
//@param $(el)
_adjustScrollExpandedBlock : function( $_block_el, adjust ) {
if ( ! $_block_el.length || _.isUndefined( this.getModuleSection() ) )
return;
var module = this,
$_moduleSection = $( '.accordion-section-content', module.section.container ),//was api.section( control.section() )
_currentScrollTopVal = $_moduleSection.scrollTop(),
_scrollDownVal,
_adjust = adjust || 90;
setTimeout( function() {
if ( ( $_block_el.offset().top + $_block_el.height() + _adjust ) > $(window.top).height() ) {
_scrollDownVal = $_block_el.offset().top + $_block_el.height() + _adjust - $(window.top).height();
if ( _scrollDownVal > 0 ) {
$_moduleSection.animate({
scrollTop: _currentScrollTopVal + _scrollDownVal
}, 500);
}
}
}, 50);
},
//close alert wrapper
//+ deactivate the icon
closeRemoveDialogs : function() {
var module = this;
if ( ! _.isArray( module.itemCollection() ) )
return;
module.czr_Item.each( function( _item_ ) {
_item_.removeDialogVisible( false );
});
// $('.' + module.control.css_attr.remove_alert_wrapper, module.container ).each( function() {
// if ( $(this).hasClass('open') ) {
// $(this).slideToggle( {
// duration : 100,
// done : function() {
// $(this).toggleClass('open' , false );
// //deactivate the icons
// $(this).siblings().find('.' + module.control.css_attr.display_alert_btn).toggleClass('active' , false );
// }
// } );
// }
// });
return this;
},
// fired when module.isReady.done
// if sortable is 'true' on registration
// default is false
_makeItemsSortable : function(obj) {
if ( wp.media.isTouchDevice || ! $.fn.sortable )
return;
var module = this;
$( '.' + module.control.css_attr.items_wrapper, module.container ).sortable( {
handle: '.' + module.control.css_attr.item_sort_handle,
start: function() {},
update: function( event, ui ) {
var _sortedCollectionReact = function() {
if ( _.has(module, 'preItem') ) {
module.preItemExpanded.set(false);
}
module.closeAllItems().closeRemoveDialogs();
var refreshPreview = function() {
api.previewer.refresh();
};
//refreshes the preview frame :
//1) only needed if transport is postMessage, because is triggered by wp otherwise
//2) only needed when : add, remove, sort item(s).
//var isItemUpdate = ( _.size(from) == _.size(to) ) && ! _.isEmpty( _.difference(from, to) );
if ( 'postMessage' == api(module.control.id).transport && ! api.CZR_Helpers.hasPartRefresh( module.control.id ) ) {
refreshPreview = _.debounce( refreshPreview, 500 );//500ms are enough
refreshPreview();
}
module.trigger( 'item-collection-sorted' );
};
module._getSortedDOMItemCollection()
.done( function( _collection_ ) {
module.itemCollection.set( _collection_ );
})
.then( function() {
_sortedCollectionReact();
});
//refreshes the preview frame, only if the associated setting is a postMessage transport one, with no partial refresh
// if ( 'postMessage' == api( module.control.id ).transport && ! api.CZR_Helpers.hasPartRefresh( module.control.id ) ) {
// _.delay( function() { api.previewer.refresh(); }, 100 );
// }
}//update
}
);
},
/*-----------------------------------------------
* TABS NAVIGATION IN ITEMS AND MODOPT
------------------------------------------------*/
//This method is fired on tab click
// IMPORTANT : the this is the item or the modopt instance. NOT the module.
// =>This method has been added to the module constructor to avoid repeating the code in two places because it is used both in items and modOpts
// @return void()
toggleTabVisibility : function( tabIdSwitchedTo ) {
var inputParent = this,
tabs = $( inputParent.container ).find('li'),
content_items = $( inputParent.container ).find('section');
//tabIdSwitchedTo = $( args.dom_event.currentTarget, args.dom_el ).attr('data-tab-id');
$( '.tabs nav li', inputParent.container ).each( function() {
$(this).removeClass('tab-current').addClass('tab-inactive');
});
$( inputParent.container ).find('li[data-tab-id="' + tabIdSwitchedTo + '"]').addClass('tab-current').removeClass('tab-inactive');
$( 'section', inputParent.container ).each( function() {
$(this).removeClass('content-current');
});
$( inputParent.container ).find('section[id="' + tabIdSwitchedTo + '"]').addClass('content-current');
},
// @return void()
// the inputParent.container (item or modOpt) is now available ar this stage
// Setup the tabs navigation
//=> Make sure the first tab is the current visible one
setupTabNav : function() {
var inputParent = this,
preProcessTabs = function() {
var dfd = $.Deferred(),
$tabs = $( '.tabs nav li', inputParent.container );
$tabs.each( function() {
$(this).removeClass('tab-current').addClass('tab-inactive');
});
$tabs.first().addClass( 'tab-current' ).removeClass('tab-inactive');
$( 'section', inputParent.container ).first().addClass( 'content-current' );
//set the layout class based on the number of tabs
var _nb = $tabs.length;
$tabs.each( function() {
$(this).addClass( _nb > 0 ? 'cols-' + _nb : '' );
});
return dfd.resolve().promise();
};
setTimeout(
function() {
preProcessTabs().done( function() {
$('.tabs', inputParent.container ).show();
});
},
20//<= introducing a small delay to let jQuery do its preprocessing job
);
}
});//$.extend
})( wp.customize , jQuery, _ );//MULTI CONTROL CLASS
//extends api.CZRModule
//
//Setup the collection of items
//renders the module view
//Listen to items collection changes and update the control setting
var CZRDynModuleMths = CZRDynModuleMths || {};
( function ( api, $, _ ) {
$.extend( CZRDynModuleMths, {
initialize: function( id, options ) {
var module = this;
api.CZRModule.prototype.initialize.call( module, id, options );
//extend the module with new template Selectors
$.extend( module, {
itemPreAddEl : ''//is specific for each crud module
} );
module.preItemsWrapper = '';//will store the pre items wrapper
//PRE MODEL VIEW STATE
// => will control the rendering / destruction of the DOM view
// => the instantiation / destruction of the input Value collection
module.preItemExpanded = new api.Value( false );
//EXTENDS THE DEFAULT MONO MODEL CONSTRUCTOR WITH NEW METHODS
//=> like remove item
//module.itemConstructor = api.CZRItem.extend( module.CZRItemDynamicMths || {} );
//default success message when item added
module.itemAddedMessage = serverControlParams.i18n.successMessage;
////////////////////////////////////////////////////
/// MODULE DOM EVENT MAP
////////////////////////////////////////////////////<
// addItem utility
// @return void()
// @param params : { dom_el : {}, dom_event : {}, event : {}, model {} }
var _doAddItem = function( params ) {
module.addItem( params ).done( function( item_id ) {
module.czr_Item( item_id , function( _item_ ) {
_item_.embedded.then( function() {
_item_.viewState( 'expanded' );
});
});
})
.fail( function( error ) {
api.errare( 'module.addItem failed on add_item', error );
});
};
module.userEventMap = new api.Value( [
//pre add new item : open the dialog box
{
trigger : 'click keydown',
selector : [ '.' + module.control.css_attr.open_pre_add_btn, '.' + module.control.css_attr.cancel_pre_add_btn ].join(','),
name : 'pre_add_item',
actions : [
'closeAllItems',
'closeRemoveDialogs',
// toggles the visibility of the Remove View Block
// => will render or destroy the pre item view
// @param : obj = { event : {}, item : {}, view : ${} }
function( params ) {
var module = this,
canWe = { addTheItem : true };
// allow remote filtering of the condition for addition
module.trigger( 'is-item-addition-possible', canWe );
// if the module has a pre-item, let's expand it, otherwise, let's add the item right away
if ( canWe.addTheItem && module.hasPreItem ) {
module.preItemExpanded.set( ! module.preItemExpanded() );
} else {
_doAddItem( params );
}
},
],
},
//add new item
{
trigger : 'click keydown',
selector : '.' + module.control.css_attr.add_new_btn, //'.czr-add-new',
name : 'add_item',
//@param params : { dom_el : {}, dom_event : {}, event : {}, model {} }
actions : function( params ) {
module.closeRemoveDialogs( params ).closeAllItems( params );
_doAddItem( params );
}
}
]);//module.userEventMap
},//initialize()
//When the control is embedded on the page, this method is fired in api.CZRBaseModuleControl:ready()
//=> right after the module is instantiated.
ready : function() {
var module = this;
//Setup the module event listeners
module.setupDOMListeners( module.userEventMap() , { dom_el : module.container } );
// Pre Item Value => used to store the preItem model
module.preItem = new api.Value( module.getDefaultItemModel() );
// Action on pre Item expansion / collapsing
module.preItemExpanded.callbacks.add( function( isExpanded ) {
if ( isExpanded ) {
module.renderPreItemView()
.done( function( $preWrapper ) {
module.preItemsWrapper = $preWrapper;
//Re-initialize the pre item model
module.preItem( module.getDefaultItemModel() );
module.trigger( 'before-pre-item-input-collection-setup' );
// Setup the pre item input collection from dom
module.setupPreItemInputCollection();
})
.fail( function( message ) {
api.errorLog( 'Pre-Item : ' + message );
});
} else {
$.when( module.preItemsWrapper.remove() ).done( function() {
module.preItem.czr_Input = {};
module.preItemsWrapper = null;
module.trigger( 'pre-item-input-collection-destroyed' );
});
}
// Expand / Collapse
module._togglePreItemViewExpansion( isExpanded );
});
api.CZRModule.prototype.ready.call( module );//fires the parent
},//ready()
//PRE MODEL INPUTS
//fired when preItem is embedded.done()
setupPreItemInputCollection : function() {
var module = this;
//Pre item input collection
module.preItem.czr_Input = new api.Values();
//creates the inputs based on the rendered items
$('.' + module.control.css_attr.pre_add_wrapper, module.container)
.find( '.' + module.control.css_attr.sub_set_wrapper)
.each( function( _index ) {
var _id = $(this).find('[data-czrtype]').attr('data-czrtype') || 'sub_set_' + _index;
//instantiate the input
module.preItem.czr_Input.add( _id, new module.inputConstructor( _id, {//api.CZRInput;
id : _id,
type : $(this).attr('data-input-type'),
container : $(this),
input_parent : module.preItem,
module : module,
is_preItemInput : true
} ) );
//fire ready once the input Value() instance is initialized
module.preItem.czr_Input( _id ).ready();
});//each
module.trigger( 'pre-item-input-collection-ready' );
},
// Intended to be overriden in a module
// introduced in July 2019 to make it simple for a multi-item module to set a default pre-item
// typically, in the slider image, this is a way to have a default image when adding an item
// @see https://github.com/presscustomizr/nimble-builder/issues/479
getPreItem : function() {
return this.preItem();
},
// overridable method introduced with the flat skope
// problem to solve in skope => an item, can't always be instantiated in a given context.
itemCanBeInstantiated : function() {
return true;
},
//Fired on user Dom action.
//the item is manually added.
//@return a promise() with the item_id as param
//@param params : { dom_el : {}, dom_event : {}, event : {}, model {} }
addItem : function( params ) {
var dfd = $.Deferred();
if ( ! this.itemCanBeInstantiated() ) {
return dfd.reject().promise();
}
var module = this,
item_candidate = module.getPreItem(),
collapsePreItem = function() {
module.preItemExpanded.set( false );
//module.toggleSuccessMessage('off');
};
if ( _.isEmpty( item_candidate ) || ! _.isObject( item_candidate ) ) {
api.errorLog( 'addItem : an item_candidate should be an object and not empty. In : ' + module.id +'. Aborted.' );
return dfd.reject().promise();
}
//display a sucess message if item_candidate is successfully instantiated
collapsePreItem = _.debounce( collapsePreItem, 200 );
//instantiates and fires ready
var _doInstantiate_ = function() {
var _item_instance_ = module.instantiateItem( item_candidate, true );//true == Added by user
if ( _.isFunction( _item_instance_ ) ) {
_item_instance_.ready();
} else {
api.errare( 'populateSavedItemCollection => Could not instantiate item in module ' + module.id , item_candidate );
}
return _item_instance_;
};
//adds it to the collection and fire item.ready()
if ( serverControlParams.isDevMode ) {
_doInstantiate_();
} else {
try { _doInstantiate_(); } catch( er ) {
api.errare( 'populateSavedItemCollection : ' + er );
return dfd.reject().promise();
}
}
if ( ! module.czr_Item.has( item_candidate.id ) ) {
return dfd.reject('populateSavedItemCollection : the item ' + item_candidate.id + ' has not been instantiated in module ' + module.id ).promise();
}
//this iife job is to close the pre item and to maybe refresh the preview
//then once done the item view is expanded to start editing it
//@return a promise()
$.Deferred( function() {
var _dfd_ = this;
module.czr_Item( item_candidate.id ).isReady.then( function() {
//module.toggleSuccessMessage('on');
collapsePreItem();
module.trigger('item-added', item_candidate );
var resolveWhenPreviewerReady = function() {
api.previewer.unbind( 'ready', resolveWhenPreviewerReady );
_dfd_.resolve();
};
//module.doActions( 'item_added_by_user' , module.container, { item : item_candidate , dom_event : params.dom_event } );
//refresh the preview frame (only needed if transport is postMessage && has no partial refresh set )
//must be a dom event not triggered
//otherwise we are in the init collection case where the items are fetched and added from the setting in initialize
// The property "refresh_on_add_item" is declared when registrating the module to the api.czrModuleMap
if ( module.refresh_on_add_item ) {
if ( 'postMessage' == api(module.control.id).transport && _.has( params, 'dom_event') && ! _.has( params.dom_event, 'isTrigger' ) && ! api.CZR_Helpers.hasPartRefresh( module.control.id ) ) {
// api.previewer.refresh().done( function() {
// _dfd_.resolve();
// });
// It would be better to wait for the refresh promise
api.previewer.bind( 'ready', resolveWhenPreviewerReady );
api.previewer.refresh();
} else {
_dfd_.resolve();
}
} else {
_dfd_.resolve();
}
});
}).always( function() {
dfd.resolve( item_candidate.id );
});
return dfd.promise();
}
});//$.extend
})( wp.customize , jQuery, _ );//MULTI CONTROL CLASS
//extends api.CZRBaseControl
//
//Setup the collection of items
//renders the module view
//Listen to items collection changes and update the module setting
var CZRDynModuleMths = CZRDynModuleMths || {};
( function ( api, $, _ ) {
$.extend( CZRDynModuleMths, {
//////////////////////////////////////////////////
/// PRE ADD MODEL DIALOG AND VIEW
//////////////////////////////////////////////////
renderPreItemView : function( obj ) {
var module = this,
dfd = $.Deferred(),
pre_add_template;
//is this view already rendered ?
if ( _.isObject( module.preItemsWrapper ) && 0 < module.preItemsWrapper.length ) { //was ! _.isEmpty( module.czr_preItem('item_content')() ) )
return dfd.resolve( module.preItemsWrapper ).promise();
}
var appendAndResolve = function( _tmpl_ ){
//console.log('pre_add_template', _tmpl_ );
//do we have an html template and a module container?
if ( _.isEmpty( _tmpl_ ) || ! module.container ) {
dfd.reject( 'renderPreItemView => Missing html template for module : '+ module.id );
}
var $_pre_add_el = $('.' + module.control.css_attr.pre_add_item_content, module.container );
$_pre_add_el.prepend( $('', { class : 'pre-item-wrapper'} ) );
$_pre_add_el.find('.pre-item-wrapper').append( _tmpl_ );
//say it
dfd.resolve( $_pre_add_el.find('.pre-item-wrapper') ).promise();
};
// do we have view template script ?
// if yes, let's use it <= Old way
// Otherwise let's fetch the html template from the server
if ( ! _.isEmpty( module.itemPreAddEl ) ) {
if ( 1 > $( '#tmpl-' + module.itemPreAddEl ).length ) {
dfd.reject( 'renderPreItemView => Missing itemPreAddEl or template in module '+ module.id );
}
// parse the html
appendAndResolve( wp.template( module.itemPreAddEl )() );
} else {
api.CZR_Helpers.getModuleTmpl( {
tmpl : 'pre-item',
module_type: module.module_type,
module_id : module.id,
control_id : module.control.id
} ).done( function( _serverTmpl_ ) {
//console.log( 'success response =>', _serverTmpl_);
appendAndResolve( api.CZR_Helpers.parseTemplate( _serverTmpl_ )() );
}).fail( function( _r_ ) {
//console.log( 'fail response =>', _r_);
dfd.reject( [ 'renderPreItemView for module : ', module.id , _r_ ].join(' ') );
});
}
return dfd.promise();
},
//@return $ el of the pre Item view
_getPreItemView : function() {
var module = this;
return $('.' + module.control.css_attr.pre_add_item_content, module.container );
},
//callback of module.preItemExpanded
//@_is_expanded = boolean.
_togglePreItemViewExpansion : function( _is_expanded ) {
var module = this,
$_pre_add_el = $( '.' + module.control.css_attr.pre_add_item_content, module.container );
//toggle it
$_pre_add_el.slideToggle( {
duration : 200,
done : function() {
var $_btn = $( '.' + module.control.css_attr.open_pre_add_btn, module.container );
$(this).toggleClass('open' , _is_expanded );
//switch icons
if ( _is_expanded )
$_btn.find('.fas').removeClass('fa-plus-square').addClass('fa-minus-square');
else
$_btn.find('.fas').removeClass('fa-minus-square').addClass('fa-plus-square');
//set the active class to the btn
$_btn.toggleClass( 'active', _is_expanded );
//set the adding_new class to the module container wrapper
$( module.container ).toggleClass( module.control.css_attr.adding_new, _is_expanded );
//make sure it's fully visible
module._adjustScrollExpandedBlock( $(this), 120 );
}//done
} );
},
toggleSuccessMessage : function( status ) {
var module = this,
_message = module.itemAddedMessage,
$_pre_add_wrapper = $('.' + module.control.css_attr.pre_add_wrapper, module.container );
$_success_wrapper = $('.' + module.control.css_attr.pre_add_success, module.container );
if ( 'on' == status ) {
//write message
$_success_wrapper.find('p').text(_message);
//set various properties
$_success_wrapper.css('z-index', 1000001 )
.css('height', $_pre_add_wrapper.height() + 'px' )
.css('line-height', $_pre_add_wrapper.height() + 'px');
} else {
$_success_wrapper.attr('style','');
}
module.container.toggleClass('czr-model-added', 'on' == status );
return this;
}
});//$.extend//CZRBaseControlMths
})( wp.customize , jQuery, _ );//BASE CONTROL CLASS
//extends api.Control
//define a set of methods, mostly helpers, to extend the base WP control class
//this will become our base constructor for main complex controls
//EARLY SETUP
var CZRBaseControlMths = CZRBaseControlMths || {};
( function ( api, $, _ ) {
$.extend( CZRBaseControlMths, {
initialize: function( id, options ) {
var control = this;
//add a shortcut to the css properties declared in the php controls
control.css_attr = _.has( serverControlParams , 'css_attr') ? serverControlParams.css_attr : {};
api.Control.prototype.initialize.call( control, id, options );
//When a partial refresh is done we need to send back all postMessage input to the preview
//=> makes sure that all post message inputs not yet saved in db are properly applied
control.bind( 'czr-partial-refresh-done', function() {
if ( _.has( control, 'czr_moduleCollection' ) ) {
_.each( control.czr_moduleCollection(), function( _mod_ ) {
if ( ! control.czr_Module( _mod_.id ) )
return;
control.czr_Module( _mod_.id ).sendModuleInputsToPreview( { isPartialRefresh : true } );
});
}
});
},
//@return void()
refreshPreview : function( obj ) {
this.previewer.refresh();
}
});//$.extend//CZRBaseControlMths
})( wp.customize , jQuery, _ );
//BASE CONTROL CLASS
//extends api.CZRBaseControl
//define a set of methods, mostly helpers, to extend the base WP control class
//this will become our base constructor for main complex controls
//EARLY SETUP
var CZRBaseModuleControlMths = CZRBaseModuleControlMths || {};
( function ( api, $, _ ) {
$.extend( CZRBaseModuleControlMths, {
initialize: function( id, options ) {
var control = this;
if ( ! api.has( id ) ) {
throw new Error( 'Missing a registered setting for control : ' + id );
}
control.czr_Module = new api.Values();
//czr_collection stores the module collection
control.czr_moduleCollection = new api.Value();
control.czr_moduleCollection.set([]);
//let's store the state of the initial module collection
control.moduleCollectionReady = $.Deferred();
//and listen to changes when it's ready
control.moduleCollectionReady.done( function( obj ) {
//if the module is not registered yet for a single module control
//=> push it to the collection now, before listening to the module collection changes
// if ( ! control.isModuleRegistered( module.id ) ) {
// control.updateModulesCollection( { module : constructorOptions } );
// }
//LISTEN TO MODULE COLLECTION
control.czr_moduleCollection.callbacks.add( function() { return control.moduleCollectionReact.apply( control, arguments ); } );
//control.removeModule( _mod );
} );
api.CZRBaseControl.prototype.initialize.call( control, id, options );
//close any open item and dialog boxes on section expansion
api.section( control.section(), function( _section_ ) {
_section_.expanded.bind(function(to) {
control.czr_Module.each( function( _mod ){
_mod.closeAllItems().closeRemoveDialogs();
if ( _.has( _mod, 'preItem' ) ) {
_mod.preItemExpanded(false);
}
});
});
});
},
//////////////////////////////////
///READY = CONTROL INSTANTIATED AND DOM ELEMENT EMBEDDED ON THE PAGE
///FIRED BEFORE API READY ? still true ?
//
// WP CORE => After the control is embedded on the page, invoke the "ready" method.
// control.deferred.embedded.done( function () {
// control.linkElements(); // Link any additional elements after template is rendered by renderContent().
// control.setupNotifications();
// control.ready();
// });
//////////////////////////////////
ready : function() {
var control = this,
single_module = {},
savedModules;
// Get the saved module and its initial items, get from the db of when dynamically registrating the setting control.
try { savedModules = control.getSavedModules(); } catch( er ) {
api.errare( 'api.CZRBaseControl::ready() => error on control.getSavedModules()', er );
control.moduleCollectionReady.reject();
return;
}
// inits the collection with the saved module => there's only one module to instantiate in this case.
// populates the collection with the saved module
_.each( control.getSavedModules() , function( _mod, _key ) {
//stores it
single_module = _mod;
//adds it to the collection
//=> it will be fired ready usually when the control section is expanded
if ( serverControlParams.isDevMode ) {
control.instantiateModule( _mod, {} );
} else {
try { control.instantiateModule( _mod, {} ); } catch( er ) {
api.errare( 'api.CZRBaseControl::Failed to instantiate module ' + _mod.id , er );
return;
}
}
//adds the module name to the control container element
control.container.attr('data-module', _mod.id );
});
//the module collection is ready
control.moduleCollectionReady.resolve( single_module );
},
//////////////////////////////////
/// VARIOUS HELPERS
//////////////////////////////////
///
//@return the default API model {} needed to instantiate a module
getDefaultModuleApiModel : function() {
//if embedded in a control, amend the common model with the section id
return {
id : '',//module.id,
module_type : '',//module.module_type,
modOpt : {},//the module modOpt property, typically high level properties that area applied to all items of the module
items : [],//$.extend( true, {}, module.items ),
crud : false,
hasPreItem : true,//a crud module has a pre item by default
refresh_on_add_item : true,// the preview is refreshed on item add
multi_item : false,
sortable : false,//<= a module can be multi-item but not necessarily sortable
control : {},//control,
section : ''
};
},
// @return the collection [] of saved module(s) to instantiate
// This method does not make sure that the module model is ready for API.
// => it just returns an array of saved module candidates to instantiate.
//
// Before instantiation, we will make sure that all required property are defined for the modules with the method control.prepareModuleForAPI()
// control : control,
// crud : bool
// id : '',
// items : [], module.items,
// modOpt : {}
// module_type : module.module_type,
// multi_item : bool
// section : module.section,
getSavedModules : function() {
var control = this,
_savedModulesCandidates = [],
_module_type = control.params.module_type,
_raw_saved_module_val = [],
_saved_items = [],
_saved_modOpt = {};
// What is the current server saved value for this setting?
// in a normal case, it should be an array of saved properties
// But it might not be if coming from a previous option system.
// => let's normalize it.
//
// First let's perform a quick check on the current saved db val.
// If the module is not multi-item, the saved value should be an object or empty if not set yet
if ( ! api.CZR_Helpers.isMultiItemModule( _module_type ) && ! _.isEmpty( api( control.id )() ) && ! _.isObject( api( control.id )() ) ) {
api.errare('api.CZRBaseControl::getSavedModules => module Control Init for ' + control.id + ' : a mono item module control value should be an object if not empty.');
}
//SPLIT ITEMS [] and MODOPT {}
//In database, items and modOpt are saved in the same option array.
//If the module has modOpt ( the slider module for example ), the modOpt are described by an object which is always unshifted at the beginning of the setting value.
//the raw DB setting value is an array : modOpt {} + the saved items :
////META IS THE FIRST ARRAY ELEMENT: A modOpt has no unique id and has the property is_modOpt set to true
//[
// is_mod_opt : true //<= inform us that this is not an item but a modOpt
//],
////THEN COME THE ITEMS
//[
// id : "czr_slide_module_0"
// slide-background : 21,
// ....
// ],
// [
// id : "czr_slide_module_1"
// slide-background : 21,
// ....
// ]
// [...]
// POPULATE THE ITEMS [] and the MODOPT {} FROM THE RAW DB SAVED SETTING VAL
// OR with the value used when registrating the module
//
// Important note :
// The items should be turned into a collection of items [].
var settingId = api.CZR_Helpers.getControlSettingId( control.id ),
settingVal = api( settingId )();
// TO FIX
if ( _.isEmpty( settingVal ) ) {
_raw_saved_module_val = [];
} else {
_raw_saved_module_val = _.isArray( settingVal ) ? settingVal : [ settingVal ];
}
_.each( _raw_saved_module_val, function( item_or_mod_opt_candidate , key ) {
if ( ! _.isObject( item_or_mod_opt_candidate ) ) {
api.errare( 'api.CZRBaseControl::::getSavedModules => an item must be an object in control ' + control.id + ' => module type => ' + control.params.module_type, _raw_saved_module_val );
return;
}
// An item or modOpt can be empty on init
// But if not empty, it has to be an associative object, with keys that are string typed
// Fixes the case where an item { null } was accepted
// https://github.com/presscustomizr/themes-customizer-fmk/issues/46
if ( ! _.isEmpty( item_or_mod_opt_candidate ) ) {
_.each( item_or_mod_opt_candidate, function( prop, _key_ ) {
if ( ! _.isString( _key_ ) ) {
api.errare( 'api.CZRBaseControl::::getSavedModules => item not well formed in control : ' + control.id + ' => module type => ' + control.params.module_type, _raw_saved_module_val );
return;
}
});
}
// Module options, if enabled, are always saved as first key
if ( api.CZR_Helpers.hasModuleModOpt( _module_type ) && 0*0 === key ) {
// a saved module mod_opt object should not have an id
if ( _.has( item_or_mod_opt_candidate, 'id') ) {
api.errare( 'api.CZRBaseControl::getSavedModules : the module ' + _module_type + ' in control ' + control.id + ' has no mod_opt defined while it should.' );
} else {
_saved_modOpt = item_or_mod_opt_candidate;
}
}
// else {
// _saved_items.push( item_or_mod_opt_candidate );
// }
//Until April 30th 2018, was :
//A modOpt has the property is_modOpt set to true
if ( ! _.has( item_or_mod_opt_candidate, 'is_mod_opt' ) ) {
_saved_items.push( item_or_mod_opt_candidate );
}
});
// This is a collection with one module
// Note : @todo : the fact that the module are saved as a collection is not relevant anymore
// This was introduced back in 2016 when building the first version of the section plugin.
// With Nimble, a control can have one module only.
_savedModulesCandidates.push({
id : api.CZR_Helpers.getOptionName( control.id ) + '_' + control.params.type,
module_type : control.params.module_type,
section : control.section(),
modOpt : $.extend( true, {} , _saved_modOpt ),//disconnect with a deep cloning
items : $.extend( true, [] , _saved_items )//disconnect with a deep cloning
});
return _savedModulesCandidates;
},
//this helper allows to check if a module has been registered in the collection
//no matter if it's not instantiated yet
isModuleRegistered : function( id_candidate ) {
var control = this;
return ! _.isUndefined( _.findWhere( control.czr_moduleCollection(), { id : id_candidate}) );
}
});//$.extend//CZRBaseControlMths
})( wp.customize , jQuery, _ );
//BASE CONTROL CLASS
//extends api.CZRBaseControl
//define a set of methods, mostly helpers, to extend the base WP control class
//this will become our base constructor for main complex controls
//EARLY SETUP
var CZRBaseModuleControlMths = CZRBaseModuleControlMths || {};
( function ( api, $, _ ) {
$.extend( CZRBaseModuleControlMths, {
//@param : module {}
//@param : constructor string
instantiateModule : function( module, constructor ) {
if ( ! _.has( module,'id') ) {
throw new Error('CZRModule::instantiateModule() : a module has no id and could not be added in the collection of : ' + this.id +'. Aborted.' );
}
var control = this;
//is a constructor provided ?
//if not try to look in the module object if we an find one
if ( _.isUndefined(constructor) || _.isEmpty(constructor) ) {
constructor = control.getModuleConstructor( module );
}
//on init, the module collection is populated with module already having an id
//For now, let's check if the id is empty and is not already part of the collection.
//@todo : improve this.
if ( ! _.isEmpty( module.id ) && control.czr_Module.has( module.id ) ) {
throw new Error('The module id already exists in the collection in control : ' + control.id );
}
var module_api_ready = control.prepareModuleForAPI( module );
//instanciate the module with the default constructor
control.czr_Module.add( module_api_ready.id, new constructor( module_api_ready.id, module_api_ready ) );
if ( ! control.czr_Module.has( module_api_ready.id ) ) {
throw new Error('instantiateModule() : instantiation failed for module id ' + module_api_ready.id + ' in control ' + control.id );
}
//return the module instance for chaining
return control.czr_Module(module_api_ready.id);
},
//@return a module constructor object
getModuleConstructor : function( module ) {
var control = this,
parentConstructor = {},
constructor = {};
if ( ! _.has( module, 'module_type' ) ) {
throw new Error('CZRModule::getModuleConstructor : no module type found for module ' + module.id );
}
if ( ! _.has( api.czrModuleMap, module.module_type ) ) {
throw new Error('Module type ' + module.module_type + ' is not listed in the module map api.czrModuleMap.' );
}
var _mthds = api.czrModuleMap[ module.module_type ].mthds,
_is_crud = api.czrModuleMap[ module.module_type ].crud,
_base_constructor = _is_crud ? api.CZRDynModule : api.CZRModule;
constructor = _base_constructor.extend( _mthds );
if ( _.isUndefined( constructor ) || _.isEmpty(constructor) || ! constructor ) {
throw new Error('CZRModule::getModuleConstructor : no constructor found for module type : ' + module.module_type +'.' );
}
return constructor;
},
//@return an API ready module object
//To be instantiated in the API, the module model must have all the required properties defined in the defaultAPIModel properly set
prepareModuleForAPI : function( module_candidate ) {
if ( ! _.isObject( module_candidate ) ) {
throw new Error('prepareModuleForAPI : a module must be an object to be instantiated.');
}
var control = this,
api_ready_module = {};
// Default module model
//{
// id : '',//module.id,
// module_type : '',//module.module_type,
// modOpt : {},//the module modOpt property, typically high level properties that area applied to all items of the module
// items : [],//$.extend( true, {}, module.items ),
// crud : false,
// hasPreItem : true,//a crud module has a pre item by default
// refresh_on_add_item : true,// the preview is refreshed on item add
// multi_item : false,
// sortable : false,//<= a module can be multi-item but not necessarily sortable
// control : {},//control,
// section : ''
// };
_.each( control.getDefaultModuleApiModel() , function( _defaultValue, _key ) {
var _candidate_val = module_candidate[_key];
switch( _key ) {
//PROPERTIES COMMON TO ALL MODULES IN ALL CONTEXTS
case 'id' :
if ( _.isEmpty( _candidate_val ) ) {
api_ready_module[_key] = control.generateModuleId( module_candidate.module_type );
} else {
api_ready_module[_key] = _candidate_val;
}
break;
case 'module_type' :
if ( ! _.isString( _candidate_val ) || _.isEmpty( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : a module type must a string not empty');
}
api_ready_module[_key] = _candidate_val;
break;
case 'items' :
if ( ! _.isArray( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : a module item list must be an array');
}
api_ready_module[_key] = _candidate_val;
break;
case 'modOpt' :
if ( ! _.isObject( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : a module modOpt property must be an object');
}
api_ready_module[_key] = _candidate_val;
break;
case 'crud' :
//get the value from the czrModuleMap
if ( _.has( api.czrModuleMap, module_candidate.module_type ) ) {
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].crud;
if ( _.isUndefined( _candidate_val ) ) {
_candidate_val = _defaultValue;
}
} else if ( ! _.isUndefined( _candidate_val) && ! _.isBoolean( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : the module param "crud" must be a boolean');
}
api_ready_module[_key] = _candidate_val || false;
break;
case 'hasPreItem' :
//get the value from the czrModuleMap
if ( _.has( api.czrModuleMap, module_candidate.module_type ) ) {
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].hasPreItem;
if ( _.isUndefined( _candidate_val ) ) {
_candidate_val = _defaultValue;
}
} else if ( ! _.isUndefined( _candidate_val) && ! _.isBoolean( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : the module param "hasPreItem" must be a boolean');
}
api_ready_module[_key] = _candidate_val || false;
break;
case 'refresh_on_add_item' :
//get the value from the czrModuleMap
if ( _.has( api.czrModuleMap, module_candidate.module_type ) ) {
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].refresh_on_add_item;
if ( _.isUndefined( _candidate_val ) ) {
_candidate_val = _defaultValue;
}
} else if ( ! _.isUndefined( _candidate_val) && ! _.isBoolean( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : the module param "refresh_on_add_item" must be a boolean');
}
api_ready_module[_key] = _candidate_val || false;
break;
case 'multi_item' :
// get the value from the czrModuleMap
// fallback on "crud" param if set
if ( _.has( api.czrModuleMap, module_candidate.module_type ) ) {
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].multi_item;
if ( _.isUndefined( _candidate_val ) ) {
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].crud;
}
} else if ( ! _.isUndefined( _candidate_val) && ! _.isBoolean( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : the module param "multi_item" must be a boolean');
}
api_ready_module[_key] = _candidate_val || false;
break;
//if the sortable property is not set, then check if crud or multi-item
case 'sortable' :
//get the value from the czrModuleMap
if ( _.has( api.czrModuleMap, module_candidate.module_type ) ) {
// if the sortable param is not specified, set it based on the "crud" and "multi_item" params
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].sortable;
if ( _.isUndefined( _candidate_val ) ) {
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].crud;
}
if ( _.isUndefined( _candidate_val ) ) {
_candidate_val = api.czrModuleMap[ module_candidate.module_type ].multi_item;
}
} else if ( ! _.isUndefined( _candidate_val) && ! _.isBoolean( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : the module param "sortable" must be a boolean');
}
api_ready_module[_key] = _candidate_val || false;
break;
case 'control' :
api_ready_module[_key] = control;//this
break;
//PROPERTIES FOR MODULE EMBEDDED IN A CONTROL
case 'section' :
if ( ! _.isString( _candidate_val ) || _.isEmpty( _candidate_val ) ) {
throw new Error('prepareModuleForAPI : a module section must be a string not empty');
}
api_ready_module[_key] = _candidate_val;
break;
//PROPERTIES FOR MODULE EMBEDDED IN A SEKTION
case 'dirty' :
api_ready_module[_key] = _candidate_val || false;
break;
}//switch
});
return api_ready_module;
},
//recursive
generateModuleId : function( module_type, key, i ) {
//prevent a potential infinite loop
i = i || 1;
if ( i > 100 ) {
throw new Error('Infinite loop when generating of a module id.');
}
var control = this;
key = key || control._getNextModuleKeyInCollection();
var id_candidate = module_type + '_' + key;
//do we have a module collection value ?
if ( ! _.has(control, 'czr_moduleCollection') || ! _.isArray( control.czr_moduleCollection() ) ) {
throw new Error('The module collection does not exist or is not properly set in control : ' + control.id );
}
//make sure the module is not already instantiated
if ( control.isModuleRegistered( id_candidate ) ) {
key++; i++;
return control.generateModuleId( module_type, key, i );
}
return id_candidate;
},
//helper : return an int
//=> the next available id of the module collection
_getNextModuleKeyInCollection : function() {
var control = this,
_max_mod_key = {},
_next_key = 0;
//get the initial key
//=> if we already have a collection, extract all keys, select the max and increment it.
//else, key is 0
if ( ! _.isEmpty( control.czr_moduleCollection() ) ) {
_max_mod_key = _.max( control.czr_moduleCollection(), function( _mod ) {
return parseInt( _mod.id.replace(/[^\/\d]/g,''), 10 );
});
_next_key = parseInt( _max_mod_key.id.replace(/[^\/\d]/g,''), 10 ) + 1;
}
return _next_key;
}
});//$.extend//CZRBaseControlMths
})( wp.customize , jQuery, _ );
//BASE CONTROL CLASS
//extends api.CZRBaseControl
//define a set of methods, mostly helpers, to extend the base WP control class
//this will become our base constructor for main complex controls
//EARLY SETUP
var CZRBaseModuleControlMths = CZRBaseModuleControlMths || {};
( function ( api, $, _ ) {
$.extend( CZRBaseModuleControlMths, {
//@return void()
//@param obj can be { collection : []}, or { module : {} }
//Can be called :
//- for all modules, in module.isReady.done() if the module is not registered in the collection yet.
//- for all modules on moduleReact ( module.callbacks )
//
//=> sets the setting value through the module collection !
updateModulesCollection : function( obj ) {
var control = this,
_current_collection = control.czr_moduleCollection(),
_new_collection = $.extend( true, [], _current_collection);
//if a collection is provided in the passed obj then simply refresh the collection
//=> typically used when reordering the collection module with sortable or when a module is removed
if ( _.has( obj, 'collection' ) ) {
//reset the collection
control.czr_moduleCollection.set( obj.collection, obj.data || {} );
return;
}
if ( ! _.has(obj, 'module') ) {
throw new Error('updateModulesCollection, no module provided ' + control.id + '. Aborting');
}
//normalizes the module for the API
var module_api_ready = control.prepareModuleForAPI( _.clone( obj.module ) );
//the module already exist in the collection
if ( _.findWhere( _new_collection, { id : module_api_ready.id } ) ) {
_.each( _current_collection , function( _elt, _ind ) {
if ( _elt.id != module_api_ready.id )
return;
//set the new val to the changed property
_new_collection[_ind] = module_api_ready;
});
}
//the module has to be added
else {
_new_collection.push( module_api_ready );
}
//WHAT ARE THE PARAMS WE WANT TO PASS TO THE NEXT ACTIONS
var _params = {};
//if a data property has been passed,
//amend the data property with the changed module
if ( _.has( obj, 'data') ) {
_params = $.extend( true, {}, obj.data );
$.extend( _params, { module : module_api_ready } );
}
//Inform the collection
control.czr_moduleCollection.set( _new_collection, _params );
},
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////// WHERE THE STREETS HAVE NO NAMES //////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// cb of control.czr_moduleCollection.callbacks
// @params is an optional object. { silent : true }
moduleCollectionReact : function( to, from, params ) {
var control = this,
is_module_added = _.size(to) > _.size(from),
is_module_removed = _.size(from) > _.size(to),
is_module_update = _.size(from) == _.size(to);
is_collection_sorted = false;
// MODULE REMOVED
// Remove the module instance if needed
if ( is_module_removed ) {
//find the module to remove
var _to_remove = _.filter( from, function( _mod ){
return _.isUndefined( _.findWhere( to, { id : _mod.id } ) );
});
_to_remove = _to_remove[0];
control.czr_Module.remove( _to_remove.id );
}
// is there a passed module param ?
// if so prepare it for DB
// if a module is provided, we also want to pass its id to the preview => can be used to target specific selectors in a partial refresh scenario
if ( _.isObject( params ) && _.has( params, 'module' ) ) {
params.module_id = params.module.id;
params.moduleRegistrationParams = params.module;
params.module = control.prepareModuleForDB( $.extend( true, {}, params.module ) );
}
// Inform the the setting if the module is not being added to the collection for the first time,
// We don't want to say it to the setting, because it might alter the setting dirtyness for nothing on init.
if ( ! is_module_added ) {
// control.filterModuleCollectionBeforeAjax( to ) returns an array of items
// if the module has modOpt, the modOpt object is always added as the first element of the items array (unshifted)
if ( serverControlParams.isDevMode ) {
api( this.id ).set( control.filterModuleCollectionBeforeAjax( to ), params );
} else {
try { api( this.id ).set( control.filterModuleCollectionBeforeAjax( to ), params ); } catch( er ) {
api.errare( 'api.CZRBaseControl::moduleCollectionReact => error when firing control.filterModuleCollectionBeforeAjax( to )', er );
}
}
//.done( function( to, from, o ) {});
}
},
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////// WHERE THE STREETS HAVE NO NAMES //////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//an overridable method to act on the collection just before it is ajaxed
//@return the collection array
filterModuleCollectionBeforeAjax : function( collection ) {
var control = this,
cloned_collection = $.extend( true, [], collection ),
_filtered_collection = [],
itemsToReturn;
_.each( cloned_collection , function( _mod, _key ) {
var db_ready_mod = $.extend( true, {}, _mod );
_filtered_collection[_key] = control.prepareModuleForDB( db_ready_mod );
});
//=> in a control : we save
//1) the collection of item(s)
//2) the modOpt
//at this point we should be in the case of a single module collection, typically use to populate a regular setting
if ( _.size( cloned_collection ) > 1 ) {
throw new Error('There should not be several modules in the collection of control : ' + control.id );
}
if ( ! _.isArray( cloned_collection ) || _.isEmpty( cloned_collection ) || ! _.has( cloned_collection[0], 'items' ) ) {
throw new Error('The setting value could not be populated in control : ' + control.id );
}
var module_id = cloned_collection[0].id;
if ( ! control.czr_Module.has( module_id ) ) {
throw new Error('The single module control (' + control.id + ') has no module registered with the id ' + module_id );
}
var module_instance = control.czr_Module( module_id );
if ( ! _.isArray( module_instance().items ) ) {
throw new Error('The module ' + module_id + ' should be an array in control : ' + control.id );
}
// items
// For a mono-item module, we save the first and unique item object
// For example :
// {
// 'heading_text' : "this is a heading"
// 'font_size' : '10px'
// ...
// }
//
// For a multi-item module, we save a collection of item objects, which may include a mod_opt item
itemsToReturn = module_instance.isMultiItem() ? module_instance().items : ( module_instance().items[0] || [] );
itemsToReturn = module_instance.filterItemsBeforeCoreApiSettingValue( itemsToReturn );
//Add the modOpt if any
return module_instance.hasModOpt() ? _.union( [ module_instance().modOpt ] , itemsToReturn ) : itemsToReturn;
},
// fired before adding a module to the collection of DB candidates
// the module must have the control.getDefaultModuleDBModel structure :
prepareModuleForDB : function ( module_db_candidate ) {
var control = this;
if ( ! _.isObject( module_db_candidate ) ) {
throw new Error('::prepareModuleForDB : a module must be an object.');
}
var db_ready_module = {};
// The items property should be a collection, even for mono-item modules
if ( ! _.isArray( module_db_candidate['items'] ) ) {
throw new Error('::prepareModuleForDB : a module item list must be an array');
}
// Let's loop on the item(s) to check if they are well formed
_.each( module_db_candidate['items'], function( itm ) {
if ( ! _.isObject( itm ) ) {
throw new Error('::prepareModuleForDB : a module item must be an object');
}
});
db_ready_module['items'] = module_db_candidate['items'];
return db_ready_module;
}
});//$.extend//CZRBaseControlMths
})( wp.customize , jQuery, _ );
( function ( api, $, _ ) {
// BASE
// BASE : Extends some constructors with the events manager
$.extend( CZRBaseControlMths, api.Events );
$.extend( api.Control.prototype, api.Events );//ensures that the default WP control constructor is extended as well
$.extend( CZRModuleMths, api.Events );
$.extend( CZRItemMths, api.Events );
$.extend( CZRModOptMths, api.Events );
// BASE : Add the DOM helpers (addAction, ...) to the Control Base Class + Input Base Class
$.extend( CZRBaseControlMths, api.CZR_Helpers );
$.extend( CZRInputMths, api.CZR_Helpers );
$.extend( CZRModuleMths, api.CZR_Helpers );
// BASE INPUTS => used as constructor when creating the collection of inputs
api.CZRInput = api.Value.extend( CZRInputMths );
// Declare all available input type as a map
api.czrInputMap = api.czrInputMap || {};
// input_type => callback fn to fire in the Input constructor on initialize
// the callback can receive specific params define in each module constructor
// For example, a content picker can be given params to display only taxonomies
// the default input_event_map can also be overriden in this callback
$.extend( api.czrInputMap, {
text : '',
textarea : '',
check : 'setupIcheck',
checkbox : 'setupIcheck',
//gutencheck : 'setupGutenCheck', // DEPRECATED since april 2nd 2019
nimblecheck : '',//setupNimbleCheck',
select : 'setupSelect',
radio : 'setupRadio',
number : 'setupStepper',
upload : 'setupImageUploaderSaveAsId',
upload_url : 'setupImageUploaderSaveAsUrl',
color : 'setupColorPicker',
wp_color_alpha : 'setupColorPickerAlpha',
wp_color : 'setupWPColorPicker',//not used for the moment
content_picker : 'setupContentPicker',
password : '',
range : 'setupSimpleRange',
range_slider : 'setupRangeSlider',
hidden : '',
h_alignment : 'setupHAlignement',
h_text_alignment : 'setupHAlignement'
});
// BASE ITEMS => used as constructor when creating the collection of models
api.CZRItem = api.Value.extend( CZRItemMths );
// BASE MODULE OPTIONS => used as constructor when creating module options
api.CZRModOpt = api.Value.extend( CZRModOptMths );
// BASE MODULES => used as constructor when creating the collection of modules
api.CZRModule = api.Value.extend( CZRModuleMths );
api.CZRDynModule = api.CZRModule.extend( CZRDynModuleMths );
// BASE CONTROLS
api.CZRBaseControl = api.Control.extend( CZRBaseControlMths );
api.CZRBaseModuleControl = api.CZRBaseControl.extend( CZRBaseModuleControlMths );
$.extend( api.controlConstructor, { czr_module : api.CZRBaseModuleControl });
})( wp.customize, jQuery, _ );
//DOM READY :
//1) FIRE SPECIFIC INPUT PLUGINS
//2) ADD SOME COOL STUFFS
//3) SPECIFIC CONTROLS ACTIONS
( function ( wp, $ ) {
$( function($) {
var api = wp.customize || api;
//WHAT IS HAPPENING IN THE MESSENGER
// $(window.parent).on( 'message', function(e, o) {
// api.consoleLog('SENT STUFFS', JSON.parse( e.originalEvent.data), e );
// });
// $( window ).on( 'message', function(e, o) {
// api.consoleLog('INCOMING MESSAGE', JSON.parse( e.originalEvent.data), e );
// });
// $(window.document).bind("ajaxSend", function(e, o){
// api.consoleLog('AJAX SEND', e, arguments );
// }).bind("ajaxComplete", function(e, o){
// api.consoleLog('AJAX COMPLETE', e, o);
// });
var fireHeaderButtons = function() {
var $header_button;
// Deactivated for the moment.
// The + button has been moved in the Nimble top bar
// if ( api.czr_sektions ) {
// var _title_ = ( window.sektionsLocalizedData && sektionsLocalizedData.i18n && sektionsLocalizedData.i18n['Drag and drop content'] ) ? sektionsLocalizedData.i18n['Drag and drop content'] : '';
// $header_button = $('', {
// class:'customize-controls-home-or-add',
// html:'Homeadd_circle_outline'
// });
// } else {
// $header_button = $('', {
// class:'customize-controls-home-or-add fas fa-home',
// html:'Home'
// });
// }
$header_button = $('', {
class:'customize-controls-home-or-add fas fa-home',
html:'Home'
});
$.when( $('#customize-header-actions').append( $header_button ) )
.done( function() {
$header_button
.keydown( function( event ) {
if ( 9 === event.which ) // tab
return;
if ( 13 === event.which ) // enter
this.click();
event.preventDefault();
})
.on( 'click.customize-controls-home-or-add', function() {
// if ( api.czr_sektions ) {
// api.previewer.trigger( 'sek-pick-content', {});
// }
//event.preventDefault();
//close everything
if ( api.section.has( api.czr_activeSectionId() ) ) {
api.section( api.czr_activeSectionId() ).expanded( false );
} else {
api.section.each( function( _s ) {
_s.expanded( false );
});
}
api.panel.each( function( _p ) {
_p.expanded( false );
});
});
// animate on init
// @use button-see-mee css class declared in core in /wp-admin/css/customize-controls.css
_.delay( function() {
if ( $header_button.hasClass( 'button-see-me') )
return;
var _seeMe = function() {
return $.Deferred(function(){
var dfd = this;
$header_button.addClass('button-see-me');
_.delay( function() {
$header_button.removeClass('button-see-me');
dfd.resolve();
}, 800 );
});
},
i = 0,
_seeMeLoop = function() {
_seeMe().done( function() {
i--;
if ( i >= 0 ) {
_.delay( function() {
_seeMeLoop();
}, 50 );
}
});
};
_seeMeLoop();
}, 2000 );
});// done()
};
fireHeaderButtons();
});//end of $( function($) ) dom ready
})( wp, jQuery );