';
// generate tab content html
var _inputCollectionHtml = renderInputCollection( tabData.inputs );
_tabContentHtml += '' + _inputCollectionHtml +'';
});
// put it all together
input_html += [
'
',
'',
'
',
_tabContentHtml,
'
',
'
',
].join('');
} else {
input_html = renderInputCollection(item_input_map);
}
// 1) Get the input map from the module registration params
// 2) Normalizes input data with defaults
// 3) loop on the input map
// 4) print the default input tmpl wrapper
return dfd.resolve( input_html ).promise();
};
})( wp.customize, jQuery );//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
cachedElements : {
$body : $('body'),
$window : $(window)
},
initialize: function() {
var self = this;
if ( _.isUndefined( window.sektionsLocalizedData ) ) {
throw new Error( 'CZRSeksPrototype => missing localized server params sektionsLocalizedData' );
}
// this class is skope dependant
if ( ! _.isFunction( api.czr_activeSkopes ) ) {
throw new Error( 'CZRSeksPrototype => api.czr_activeSkopes' );
}
// SECTIONS ID FOR LOCAL AND GLOBAL OPTIONS
self.SECTION_ID_FOR_GLOBAL_OPTIONS = '__globalOptionsSectionId';
self.SECTION_ID_FOR_LOCAL_OPTIONS = '__localOptionsSection';
// SECTION ID FOR THE CONTENT PICKER
self.SECTION_ID_FOR_CONTENT_PICKER = '__content_picker__';
// Max possible number of columns in a section
self.MAX_NUMBER_OF_COLUMNS = 12;
// _.debounce param when updating the UI setting
// prevent hammering server + fixes https://github.com/presscustomizr/nimble-builder/issues/244
self.SETTING_UPDATE_BUFFER = 100;
// introduced for https://github.com/presscustomizr/nimble-builder/issues/403
self.TINYMCE_EDITOR_HEIGHT = 100;
// Define a default value for the sektion setting value, used when no server value has been sent
// @see php function
// function sek_get_default_location_model() {
// $defaut_sektions_value = [ 'collection' => [], 'options' => [] ];
// foreach( sek_get_locations() as $location ) {
// $defaut_sektions_value['collection'][] = [
// 'id' => $location,
// 'level' => 'location',
// 'collection' => [],
// 'options' => []
// ];
// }
// return $defaut_sektions_value;
// }
self.defaultLocalSektionSettingValue = self.getDefaultSektionSettingValue( 'local' );
// Store the contextual setting prefix
self.localSectionsSettingId = new api.Value( {} );
// Keep track of the registered ui elements dynamically registered
// this collection is populated in ::register(), if the track param is true
// this is used to know what ui elements are currently being displayed
self.registered = new api.Value([]);
// June 2020 : added for https://github.com/presscustomizr/nimble-builder/issues/708
if ( wp.customize.apiIsReady ) {
self.doSektionThinksOnApiReady();
} else {
api.bind( 'ready', function() {
self.doSektionThinksOnApiReady();
});
}
// Add the skope id on save
// Uses a WP core hook to filter the query on a customize_save action
//
// This posted skope id is useful when we need to know the skope id during ajax.
// ( Note that with the nimble ajax action, the skope_id is always posted. Not in WP core ajax actions. )
// Example of use of $_POST['local_skope_id'] => @see sek_get_parent_level_model()
// Helps fixing : https://github.com/presscustomizr/nimble-builder/issues/242, for which sek_add_css_rules_for_spacing() couldn't be set for columns margins
api.bind( 'save-request-params', function( query ) {
$.extend( query, {
local_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),
group_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id', 'group' ),//<= feb 2021, added for #478
active_locations : api.czr_sektions.activeLocations(),
inherit_group_template : true
});
});
// TINY MCE EDITOR
var clearActiveWPEditorsInstances = function() {
if ( _.isArray( api.czrActiveWPEditors ) ) {
_.each( api.czrActiveWPEditors, function( _id ) {
wp.oldEditor.remove( _id );
});
api.czrActiveWPEditors = [];
}
};
// added for https://github.com/presscustomizr/nimble-builder/issues/403
// in fmk::setupTinyMceEditor => each id of newly instantiated editor is added to the [] api.czrActiveWPEditors
// We need to remove those instances when cleaning registered controls
api.bind( 'sek-before-clean-registered', clearActiveWPEditorsInstances );
// 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
// 'czr-all-items-closed' is fired in CZRModuleMths.closeAllItems()
api.bind('czr-all-items-closed', clearActiveWPEditorsInstances );
},// initialize()
// @ API READY
// Fired at api.bind( 'ready', function() {})
doSektionThinksOnApiReady : function() {
var self = this;
// the main sektion panel
// the local and global options section
self.registerAndSetupDefaultPanelSectionOptions();
// Setup the collection settings => register the main settings for local and global skope and bind it
// schedule reaction to collection setting ids => the setup of the collection setting when the collection setting ids are set
//=> on skope change
//@see setContextualCollectionSettingIdWhenSkopeSet
//
// var _settingsToRegister_ = {
// 'local' : { collectionSettingId : self.localSectionsSettingId() },//<= "nimble___[skp__post_page_10]"
// 'global' : { collectionSettingId : self.getGlobalSectionsSettingId() }//<= "nimble___[skp__global]"
// };
self.localSectionsSettingId.callbacks.add( function( collectionSettingIds, previousCollectionSettingIds ) {
// register the collection setting id
// and schedule the reaction to different collection changes : refreshModules, ...
try { self.setupSettingsToBeSaved(); } catch( er ) {
api.errare( 'Error in self.localSectionsSettingId.callbacks => self.setupSettingsToBeSaved()' , er );
}
// Now that the local and global settings are registered, initialize the history log
self.initializeHistoryLogWhenSettingsRegistered();
// On init and when skope changes, request the contextually active locations
// We should not need this call, because the preview sends it on initialize
// But this is safer.
// The preview send back the list of active locations 'sek-active-locations-in-preview'
// introduced for the level tree, https://github.com/presscustomizr/nimble-builder/issues/359
api.previewer.send('sek-request-active-locations');
});
// POPULATE THE MAIN SETTING ID NOW
// + GENERATE UI FOR THE LOCAL SKOPE OPTIONS
// + GENERATE UI FOR THE GLOBAL OPTIONS
// + GENERATE UI FOR SITE TEMPLATES
var doSkopeDependantActions = function( newSkopes, previousSkopes ) {
self.setContextualCollectionSettingIdWhenSkopeSet( newSkopes, previousSkopes );
// Generate UI for the local skope options
api.section( self.SECTION_ID_FOR_LOCAL_OPTIONS, function( _section_ ) {
_section_.deferred.embedded.done( function() {
if( true === _section_.boundForLocalOptionGeneration )
return;
// Defer the UI generation when the section is expanded
_section_.boundForLocalOptionGeneration = true;
_section_.expanded.bind( function( expanded ) {
if ( true === expanded ) {
self.generateUI({ action : 'sek-generate-local-skope-options-ui'});
}
});
});
});
// The UI of the global option must be generated only once.
// We don't want to re-generate on each skope change
// fixes https://github.com/presscustomizr/nimble-builder/issues/271
api.section( self.SECTION_ID_FOR_GLOBAL_OPTIONS, function( _section_ ) {
if ( true === _section_.nimbleGlobalOptionGenerated )
return;
self.generateUI({ action : 'sek-generate-global-options-ui'});
_section_.nimbleGlobalOptionGenerated = true;
// Make sure template gallery is closed when opening/closing global options panel
// see https://github.com/presscustomizr/nimble-builder/issues/840
_section_.expanded.bind( function() {
if ( !self.templateGalleryExpanded )
return;
self.templateGalleryExpanded(false);
});
});
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
// June 2020 property introduced for https://github.com/presscustomizr/nimble-builder-pro/issues/12
self.nb_is_ready = true;
// This event has been introduced when implementing https://github.com/presscustomizr/nimble-builder/issues/304
api.trigger('nimble-ready-for-current-skope');
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
};//doSkopeDependantActions()
// populate the setting ids now if skopes are set
if ( !_.isEmpty( api.czr_activeSkopes().local ) ) {
doSkopeDependantActions();
}
// ON SKOPE READY
// - Set the contextual setting prefix
// - Generate UI for Nimble local skope options
// - Generate the content picker
api.czr_activeSkopes.callbacks.add( function( newSkopes, previousSkopes ) {
doSkopeDependantActions( newSkopes, previousSkopes );
});
// Communicate with the preview
self.reactToPreviewMsg();
// Setup Dnd
self.setupDnd();
// setup the tinyMce editor used for the tiny_mce_editor input
// => one object listened to by each tiny_mce_editor input
self.setupTinyMceEditor();
// print json
self.schedulePrintSectionJson();
// Always set the previewed device back to desktop on ui change
// event 'sek-ui-removed' id triggered when cleaning the registered ui controls
// @see ::cleanRegisteredAndLargeSelectInput()
// July 2020 commented to fix https://github.com/presscustomizr/nimble-builder/issues/728
// self.bind( 'sek-ui-removed', function() {
// api.previewedDevice( 'desktop' );
// });
// Synchronize api.previewedDevice with the currently rendered ui
// ensure that the selected device tab of the spacing module is the one being previewed
// =>@see spacing module, in item constructor CZRSpacingItemMths
api.previewedDevice.bind( function( device ) {
var currentControls = _.filter( self.registered(), function( uiData ) {
return 'control' == uiData.what;
});
_.each( currentControls || [] , function( ctrlData ) {
api.control( ctrlData.id, function( _ctrl_ ) {
_ctrl_.container.find('[data-sek-device="' + device + '"]').each( function() {
$(this).trigger('click');
});
});
});
// Send the previewed device to the preview
//api.previewer.send( 'sek-preview-device-changed', { device : device });
});
// Schedule a reset
$('#customize-notifications-area').on( 'click', '[data-sek-reset="true"]', function() {
api.previewer.trigger('sek-reset-collection', { scope : 'local' } );
});
// CLEAN UI BEFORE REMOVAL
// 'sek-ui-pre-removal' is triggered in ::cleanRegisteredAndLargeSelectInput
// @params { what : control, id : '' }
self.bind( 'sek-ui-pre-removal', function( params ) {
// CLEAN DRAG N DROP
if ( 'control' == params.what && -1 < params.id.indexOf( 'draggable') ) {
api.control( params.id, function( _ctrl_ ) {
_ctrl_.container.find( '[draggable]' ).each( function() {
$(this).off( 'dragstart dragend' );
});
});
}
// CLEAN SELECT2
// => we need to destroy the czrSelect2 instance, otherwise it can stay open when switching to another ui.
if ( 'control' == params.what ) {
api.control( params.id, function( _ctrl_ ) {
_ctrl_.container.find( 'select' ).each( function() {
if ( ! _.isUndefined( $(this).data('czrSelect2') ) ) {
$(this).czrSelect2('destroy');
}
});
});
}
});
// POPULATE THE REGISTERED COLLECTION
// 'czr-new-registered' is fired in api.CZR_Helpers.register()
api.bind( 'czr-new-registered', function( params ) {
//console.log( 'czr-new-registered => ', params );
// Check that we have an origin property and that make sure we populate only the registration emitted by 'nimble'
if ( _.isUndefined( params.origin ) ) {
throw new Error( 'czr-new-registered event => missing params.origin' );
}
if ( 'nimble' !== params.origin )
return;
// when no collection is provided, we use
if ( false !== params.track ) {
var currentlyRegistered = self.registered();
var newRegistered = $.extend( true, [], currentlyRegistered );
//Check for duplicates
var duplicateCandidate = _.findWhere( newRegistered, { id : params.id } );
if ( ! _.isEmpty( duplicateCandidate ) && _.isEqual( duplicateCandidate, params ) ) {
throw new Error( 'register => duplicated element in self.registered() collection ' + params.id );
}
newRegistered.push( params );
self.registered( newRegistered );
// say it
//this.trigger( [params.what, params.id , 'registered' ].join('__'), params );
}
});
// store active locations
self.activeLocations = new api.Value([]);// <= introduced for the level tree, https://github.com/presscustomizr/nimble-builder/issues/359
self.activeLocationsInfo = new api.Value([]);// <= introduced for better move up/down of sections https://github.com/presscustomizr/nimble-builder/issues/521
api.previewer.bind('sek-active-locations-in-preview', function( activelocs ){
self.activeLocations( ( _.isObject(activelocs) && _.isArray( activelocs.active_locations ) ) ? activelocs.active_locations : [] );
self.activeLocationsInfo( ( _.isObject(activelocs) && _.isArray( activelocs.active_locs_info ) ) ? activelocs.active_locs_info : [] );
// December 2020 => refresh local setting when an active location is available locally but not present in the local setting
// Fixes the problem of importing template from the gallery, with locations different than the current local page
// update : December 24th => deactivated because of https://github.com/presscustomizr/nimble-builder/issues/770
// if ( !_.isEmpty( api.dirtyValues() ) ) {
// try{ self.updateAPISetting({ action : 'sek-maybe-add-missing-locations'}); } catch(er) {
// api.errare( '::initialize => error with sek-maybe-add-missing-locations', er );
// }
// }
});
// TOP BAR
// Setup the topbar including do/undo action buttons
self.setupTopBar();//@see specific dev file
// SAVE SECTION UI
// June 2020 : for https://github.com/presscustomizr/nimble-builder/issues/520 and https://github.com/presscustomizr/nimble-builder/issues/713
self.setupSaveSectionUI();
// SAVE TEMPLATE UI
// April 2020 : introduced for https://github.com/presscustomizr/nimble-builder/issues/655
self.setupSaveTmplUI();
// SETUP DOUBLE CLICK INSERTION THINGS
// Stores the preview target for double click insertion
// implemented for https://github.com/presscustomizr/nimble-builder/issues/317
self.lastClickedTargetInPreview = new api.Value();
self.lastClickedTargetInPreview.bind( function( to, from ) {
// to and from are formed this way : { id : "__nimble__fb2ab3e47472" }
// @see 'sek-pick-content' event in ::reactToPreviewMsg()
// Send the level id of the current double-click insertion target
// => this will be used to style the level id container with a pulse animation
if ( _.isObject( to ) && to.id ) {
api.previewer.send( 'sek-set-double-click-target', to );
} else {
// Tell the preview to clean the target highlight effect
api.previewer.send( 'sek-reset-double-click-target' );
}
// reset after a delay
clearTimeout( self.cachedElements.$window.data('_preview_target_timer_') );
self.cachedElements.$window.data('_preview_target_timer_', setTimeout(function() {
// Reset the click target
self.lastClickedTargetInPreview( {} );
// Tell the preview to clean the target highlight effect
api.previewer.send( 'sek-reset-double-click-target' );
}, 20000 ) );
});
// React to the preview to clean any currently highlighted drop zone
// This event is triggered on all click in the preview iframe
// @see preview::scheduleUiClickReactions()
api.previewer.bind( 'sek-clean-target-drop-zone', function() {
// Reset the click target
self.lastClickedTargetInPreview({});
});
// Clean the current target when hitting escape
$(document).keydown(function( evt ) {
// ESCAPE key pressed
if ( evt && 27 === evt.keyCode ) {
self.lastClickedTargetInPreview({});
}
});
// PRINT A WARNING NOTICE FOR USERS OF CACHE PLUGIN
if ( sektionsLocalizedData.hasActiveCachePlugin ) {
_.delay( function() {
api.previewer.trigger('sek-notify', {
notif_id : 'has-active-cache-plugin',
type : 'info',
duration : 20000,
message : [
'',
sektionsLocalizedData.i18n['You seem to be using a cache plugin.'],
( ! _.isString( sektionsLocalizedData.hasActiveCachePlugin ) || sektionsLocalizedData.hasActiveCachePlugin.length < 2 ) ? '' : ' (' + sektionsLocalizedData.hasActiveCachePlugin + ') ',
' ',
sektionsLocalizedData.i18n['It is recommended to disable your cache plugin when customizing your website.'],
'',
''
].join('')
});
}, 2000 );//delay()
}
// SCHEDULE AN AUTOFOCUS ON THE ITEM THAT HAS BEEN MODIFIED IN THE PREVIEW
// the 'multi-items-module-refreshed' event is sent on each preview update due to a Nimble change
// @see sendSuccessDataToPanel() in SekPreviewPrototype::schedulePanelMsgReactions
api.previewer.bind('multi-items-module-refreshed', function( params ) {
if ( _.isUndefined( params.apiParams.control_id ) )
return;
// the module_id param is added on control registration
// @see CZRSeksPrototype::generateUIforFrontModules
// we use it to identify that
api.control( params.apiParams.control_id, function( _control_ ) {
if ( _.isUndefined( _control_.params.sek_registration_params ) )
return;
if ( api.control( _control_.id ).params.sek_registration_params.module_id !== params.apiParams.id )
return;
_control_.czr_Module.each( function( _module_ ) {
_module_.czr_Item.each( function( _item_ ) {
if( 'expanded' === _item_.viewState() ) {
_item_.trigger('sek-request-item-focus-in-preview');
}
});
});
});
});//api.previewer.bind()
// April 2020. For https://github.com/presscustomizr/nimble-builder/issues/651
self.setupTemplateGallery();
// March 2021. For #478. 'czr-new-skopes-synced' is sent by preview on each refresh
api.previewer.bind( 'czr-new-skopes-synced', function( skope_server_data ) {
//console.log('SKOPED SYNCED');
var localSektionsData = api.czr_skopeBase.getSkopeProperty( 'sektions', 'local');
if ( sektionsLocalizedData.isDevMode ) {
api.infoLog( '::czr-new-skopes-synced => SEKTIONS DATA ? ', localSektionsData );
}
if ( _.isEmpty( localSektionsData ) ) {
api.errare('::czr-new-skopes-synced => no sektionsData');
}
if ( _.isEmpty( localSektionsData.setting_id ) ) {
api.errare('::czr-new-skopes-synced => missing setting_id');
}
});
},//doSektionThinksOnApiReady
// Fired at api "ready"
registerAndSetupDefaultPanelSectionOptions : function() {
var self = this;
// MAIN SEKTION PANEL
var SektionPanelConstructor = api.Panel.extend({
//attachEvents : function () {},
// Always make the panel active, event if we have no sections / control in it
isContextuallyActive : function () {
return this.active();
},
_toggleActive : function(){ return true; }
});
// Prepend the Nimble logo in the main panel title
// the panel.expanded() Value is not the right candidate to be observed because it gets changed on too many events, when generating the various UI.
api.panel( sektionsLocalizedData.sektionsPanelId, function( _mainPanel_ ) {
_mainPanel_.deferred.embedded.done( function() {
var $sidePanelTitleEl = _mainPanel_.container.first().find('h3.accordion-section-title'),
$topPanelTitleEl = _mainPanel_.container.first().find('.panel-meta .accordion-section-title'),
logoHtml = [
'',
].join('');
// Add Pro
if ( sektionsLocalizedData.isPro ) {
logoHtml += [
'',
].join('');
}
if ( 0 < $sidePanelTitleEl.length ) {
// The default title looks like this : Nimble Builder Press return or enter to open this section
// we want to style "Nimble Builder" only.
var $sidePanelTitleElSpan = $sidePanelTitleEl.find('span');
$sidePanelTitleEl
.addClass('sek-side-nimble-logo-wrapper')
.html( logoHtml )
.append( $sidePanelTitleElSpan );
}
// default looks like
// You are customizing Nimble Builder
// if ( 0 < $topPanelTitleEl.length ) {
// var $topPanelTitleElInner = $topPanelTitleEl.find('.panel-title');
// $topPanelTitleElInner.html( logoHtml );
// }
// NOV FEEDBACK UI DISABLED in favor of an admin notice
// if ( sektionsLocalizedData.eligibleForFeedbackNotification ) {
// _mainPanel_.expanded.bind( function( expanded ) {
// if ( expanded && _.isUndefined( self.feedbackUIVisible ) ) {
// // FEEDBACK UI
// self.setupFeedBackUI();
// // march 2020 : print confettis
// // confettis script is enqueued in the preview
// setTimeout( function() {
// api.previewer.send('sek-print-confettis', { duration : Date.now() + (1 * 2000) } );
// }, 1000 );
// }
// });
// }//if ( sektionsLocalizedData.eligibleForFeedbackNotification )
});
});
// The parent panel for all ui sections + global options section
api.CZR_Helpers.register({
origin : 'nimble',
what : 'panel',
id : sektionsLocalizedData.sektionsPanelId,//'__sektions__'
title: sektionsLocalizedData.i18n['Nimble Builder'],
priority : -1000,
constructWith : SektionPanelConstructor,
track : false,//don't register in the self.registered() => this will prevent this container to be removed when cleaning the registered
});
//GLOBAL OPTIONS SECTION
api.CZR_Helpers.register({
origin : 'nimble',
what : 'section',
id : self.SECTION_ID_FOR_GLOBAL_OPTIONS,
title: sektionsLocalizedData.i18n['Site wide options'],
panel : sektionsLocalizedData.sektionsPanelId,
priority : 20,
track : false,//don't register in the self.registered() => this will prevent this container to be removed when cleaning the registered
constructWith : api.Section.extend({
//attachEvents : function () {},
// Always make the section active, event if we have no control in it
isContextuallyActive : function () {
return this.active();
},
_toggleActive : function(){ return true; }
})
}).done( function() {
api.section( self.SECTION_ID_FOR_GLOBAL_OPTIONS, function( _section_ ) {
// Style the section title
var $sectionTitleEl = _section_.container.find('.accordion-section-title'),
$panelTitleEl = _section_.container.find('.customize-section-title h3');
// The default title looks like this : Title Press return or enter to open this section
if ( 0 < $sectionTitleEl.length ) {
$sectionTitleEl.prepend( '' );
}
// The default title looks like this : Customizing Title
if ( 0 < $panelTitleEl.length ) {
$panelTitleEl.find('.customize-action').after( '' );
}
// Schedule the accordion behaviour
self.scheduleModuleAccordion.call( _section_ );
});
});
//LOCAL OPTIONS SECTION
api.CZR_Helpers.register({
origin : 'nimble',
what : 'section',
id : self.SECTION_ID_FOR_LOCAL_OPTIONS,//<= the section id doesn't need to be skope dependant. Only the control id is skope dependant.
title: sektionsLocalizedData.i18n['Current page options'],
panel : sektionsLocalizedData.sektionsPanelId,
priority : 10,
track : false,//don't register in the self.registered() => this will prevent this container to be removed when cleaning the registered
constructWith : api.Section.extend({
//attachEvents : function () {},
// Always make the section active, event if we have no control in it
isContextuallyActive : function() {
return this.active();
},
_toggleActive : function(){ return true; }
})
}).done( function() {
api.section( self.SECTION_ID_FOR_LOCAL_OPTIONS, function( _section_ ) {
// Style the section title
var $sectionTitleEl = _section_.container.find('.accordion-section-title'),
$panelTitleEl = _section_.container.find('.customize-section-title h3');
// The default title looks like this : Title Press return or enter to open this section
if ( 0 < $sectionTitleEl.length ) {
$sectionTitleEl.prepend( '' );
}
// The default title looks like this : Customizing Title
if ( 0 < $panelTitleEl.length ) {
$panelTitleEl.find('.customize-action').after( '' );
}
// Schedule the accordion behaviour
self.scheduleModuleAccordion.call( _section_ );
});
});
// SITE WIDE GLOBAL OPTIONS SETTING
// Will Be updated in ::generateUIforGlobalOptions()
// has no control.
api.CZR_Helpers.register( {
origin : 'nimble',
//level : params.level,
what : 'setting',
id : sektionsLocalizedData.optNameForGlobalOptions,
dirty : false,
value : sektionsLocalizedData.globalOptionDBValues,
transport : 'postMessage',//'refresh',//// ,
type : 'option'
});
// CONTENT PICKER SECTION
api.CZR_Helpers.register({
origin : 'nimble',
what : 'section',
id : self.SECTION_ID_FOR_CONTENT_PICKER,
title: sektionsLocalizedData.i18n['Content Picker'],
panel : sektionsLocalizedData.sektionsPanelId,
priority : 30,
track : false,//don't register in the self.registered() => this will prevent this container to be removed when cleaning the registered
constructWith : api.Section.extend({
//attachEvents : function () {},
// Always make the section active, event if we have no control in it
isContextuallyActive : function() {
return this.active();
},
_toggleActive : function(){ return true; }
})
}).done( function() {
// generate the UI for the content picker if not done yet
// defer this action when the section is instantiated AND the api.previewer is active, so we can trigger event on it
// => we also need the local skope to be set, that's why api.czr_initialSkopeCollectionPopulated is convenient because it ensures the api.previewer is ready and we have a local skope set.
// @see czr-skope-base.js
// @fixes https://github.com/presscustomizr/nimble-builder/issues/187
api.section( self.SECTION_ID_FOR_CONTENT_PICKER, function( _section_ ) {
if ( 'resolved' != api.czr_initialSkopeCollectionPopulated.state() ) {
api.czr_initialSkopeCollectionPopulated.done( function() {
api.previewer.trigger('sek-pick-content', { focus : false });
});
} else {
api.previewer.trigger('sek-pick-content', { focus : false });
}
});
});
},//registerAndSetupDefaultPanelSectionOptions()
//@return void()
// sektionsData is built server side :
//array(
// 'db_values' => sek_get_skoped_seks( $skope_id ),
// 'setting_id' => sek_get_seks_setting_id( $skope_id )//nimble___[skp__post_page_home]
// )
setContextualCollectionSettingIdWhenSkopeSet : function( newSkopes, previousSkopes ) {
var self = this;
previousSkopes = previousSkopes || {};
// Clear all previous sektions if the main panel is expanded and we're coming from a previousSkopes
if ( !_.isEmpty( previousSkopes.local ) && api.panel( sektionsLocalizedData.sektionsPanelId ).expanded() ) {
// We don't want to change focus to content picker when setting site templates ( which forces a preview refresh on home when being opened and modified )
if ( _.isUndefined(api._nimbleRefreshingPreviewHomeWhenSettingSiteTemplate) || !api._nimbleRefreshingPreviewHomeWhenSettingSiteTemplate ) {
api.previewer.trigger('sek-pick-content');
}
}
// set the localSectionsSettingId now, and update it on skope change
sektionsData = api.czr_skopeBase.getSkopeProperty( 'sektions', 'local');
if ( sektionsLocalizedData.isDevMode ) {
api.infoLog( '::setContextualCollectionSettingIdWhenSkopeSet => SEKTIONS DATA ? ', sektionsData );
}
if ( _.isEmpty( sektionsData ) ) {
api.errare('::setContextualCollectionSettingIdWhenSkopeSet() => no sektionsData');
}
if ( _.isEmpty( sektionsData.setting_id ) ) {
api.errare('::setContextualCollectionSettingIdWhenSkopeSet() => missing setting_id');
}
self.localSectionsSettingId( sektionsData.setting_id );
}
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// fired in ::initialize(), at api.bind( 'ready', function() {})
setupTopBar : function() {
var self = this;
self.topBarId = '#nimble-top-bar';
self.topBarVisible = new api.Value( false );
self.topBarVisible.bind( function( visible ){
if ( ! self.levelTreeExpanded() ) {
self.toggleTopBar( visible );
}
});
self.mouseMovedRecently = new api.Value( {} );
self.mouseMovedRecently.bind( function( position ) {
self.topBarVisible( ! _.isEmpty( position ) );
});
var trackMouseMovements = function( evt ) {
self.mouseMovedRecently( { x : evt.clientX, y : evt.clientY } );
clearTimeout( self.cachedElements.$window.data('_scroll_move_timer_') );
self.cachedElements.$window.data('_scroll_move_timer_', setTimeout(function() {
self.mouseMovedRecently.set( {} );
}, 4000 ) );
};
self.cachedElements.$window.on( 'mousemove scroll,', _.throttle( trackMouseMovements , 50 ) );
api.previewer.bind('ready', function() {
$(api.previewer.targetWindow().document ).on( 'mousemove scroll,', _.throttle( trackMouseMovements , 50 ) );
});
// LEVEL TREE
self.setupLevelTree();
},
// @return void()
// self.topBarVisible.bind( function( visible ){
// self.toggleTopBar( visible );
// });
toggleTopBar : function( visible ) {
visible = _.isUndefined( visible ) ? true : visible;
var self = this,
_renderAndSetup = function() {
$.when( self.renderAndSetupTopBarTmpl({}) ).done( function( $_el ) {
self.topBarContainer = $_el;
//display
_.delay( function() {
self.cachedElements.$body.addClass('nimble-top-bar-visible');
}, 200 );
});
},
_hide = function() {
var dfd = $.Deferred();
self.cachedElements.$body.removeClass('nimble-top-bar-visible');
if ( self.topBarContainer && self.topBarContainer.length ) {
//remove Dom element after slide up
_.delay( function() {
//self.topBarContainer.remove();
dfd.resolve();
}, 300 );
} else {
dfd.resolve();
}
return dfd.promise();
};
if ( visible ) {
_renderAndSetup();
} else {
_hide().done( function() {
self.topBarVisible( false );//should be already false
});
}
},
//@param = { }
renderAndSetupTopBarTmpl : function( params ) {
var self = this,
_tmpl;
// CHECK IF ALREADY RENDERED
if ( $( self.topBarId ).length > 0 )
return $( self.topBarId );
// RENDER
try {
_tmpl = wp.template( 'nimble-top-bar' )( {} );
} catch( er ) {
api.errare( 'Error when parsing the the top note template', er );
return false;
}
$('#customize-preview').after( $( _tmpl ) );
$('#customize-preview').trigger('nimble-top-bar-rendered');
// UNDO / REDO ON CTRL + Z / CTRL + Y EVENTS
$(document).keydown( function( evt ) {
if ( evt.ctrlKey && _.contains( [89, 90], evt.keyCode ) ) {
try { self.navigateHistory( 90 === evt.keyCode ? 'undo' : 'redo'); } catch( er ) {
api.errare( 'Error when firing self.navigateHistory', er );
}
}
});
// CLICK EVENTS
// Attach click events
$('.sek-add-content', self.topBarId).on( 'click', function(evt) {
evt.preventDefault();
api.previewer.trigger( 'sek-pick-content', { content_type : 'module' });
});
$('.sek-level-tree', self.topBarId).on( 'click', function(evt) {
evt.preventDefault();
self.levelTreeExpanded(!self.levelTreeExpanded());
});
$('[data-nimble-history]', self.topBarId).on( 'click', function(evt) {
try { self.navigateHistory( $(this).data( 'nimble-history') ); } catch( er ) {
api.errare( 'Error when firing self.navigateHistory', er );
}
});
$('.sek-settings', self.topBarId).on( 'click', function(evt) {
// Focus on the Nimble panel
api.panel( sektionsLocalizedData.sektionsPanelId, function( _panel_ ) {
self.rootPanelFocus();
_panel_.focus();
});
// // Generate UI for the local skope options
// self.generateUI({ action : 'sek-generate-local-skope-options-ui'}).done( function() {
// api.control( self.getLocalSkopeOptionId(), function( _control_ ) {
// _control_.focus();
// });
// });
});
$('.sek-nimble-doc, .sek-notifications', self.topBarId).on( 'click', function(evt) {
evt.preventDefault();
if ( $(this).data('doc-href') ) {
window.open($(this).data('doc-href'), '_blank');
}
});
$('.sek-tmpl-saving', self.topBarId ).on( 'click', function(evt) {
// Focus on the Nimble panel
// api.panel( sektionsLocalizedData.sektionsPanelId, function( _panel_ ) {
// self.rootPanelFocus();
// _panel_.focus();
// });
evt.preventDefault();
self.tmplDialogVisible(!self.tmplDialogVisible());// self.tmplDialogVisible() is initialized false
});
$( self.topBarId ).on( 'click', '.sek-reset-local-sektions', function(evt) {
// Focus on the Nimble panel
// api.panel( sektionsLocalizedData.sektionsPanelId, function( _panel_ ) {
// self.rootPanelFocus();
// _panel_.focus();
// });
// api.control( self.getLocalSkopeOptionId() + '__local_reset', function( _control_ ) {
// _control_.focus();
// _control_.container.find('.customize-control-title').trigger('click');
// });
// evt.preventDefault();
// Focus on the Nimble panel
api.panel( sektionsLocalizedData.sektionsPanelId, function( _panel_ ) {
self.rootPanelFocus();
_panel_.focus();
api.section( self.SECTION_ID_FOR_LOCAL_OPTIONS, function( _section_ ) {
_section_.focus();
setTimeout( function() {
api.control( self.getLocalSkopeOptionId() + '__local_reset', function( _control_ ) {
_control_.focus();
_control_.container.find('.customize-control-title').trigger('click');
_control_.container.addClass('button-see-me');
_.delay( function() {
_control_.container.removeClass('button-see-me');
}, 800 );
});
}, 500 );
});
});
});
$( self.topBarId ).on( 'click', '.sek-goto-site-tmpl-options', function(evt) {
// evt.preventDefault();
// Focus on the Nimble panel
api.panel( sektionsLocalizedData.sektionsPanelId, function( _panel_ ) {
self.rootPanelFocus();
_panel_.focus();
api.section( self.SECTION_ID_FOR_GLOBAL_OPTIONS, function( _section_ ) {
_section_.focus();
setTimeout( function() {
api.control( sektionsLocalizedData.prefixForSettingsNotSaved + sektionsLocalizedData.optNameForGlobalOptions + '__site_templates', function( _control_ ) {
_control_.focus();
_control_.container.find('.customize-control-title').trigger('click');
});
}, 500 );
});
});
});
// NOTIFICATION WHEN USING CUSTOM TEMPLATE
// implemented for https://github.com/presscustomizr/nimble-builder/issues/304
var printSektionsSkopeStatus = function( args ) {
if ( $(self.topBarId).length < 1 || sektionsLocalizedData.isDebugMode )
return;
var _hasLocalNBCustomizations = false;
if ( args && args.on_init ) {
//console.log('ON INIT : ', api.czr_skopeBase.getSkopeProperty( 'skope_id', 'group' ) );
_hasLocalNBCustomizations = api.czr_skopeBase.getSkopeProperty( 'has_local_nimble_customizations', 'local' );//property added server side see php:add_sektion_values_to_skope_export()
} else if ( args && args.after_reset ) {
//console.log('AFTER RESET');
_hasLocalNBCustomizations = false;
} else {
_hasLocalNBCustomizations = self.hasLocalSettingBeenCustomized();
}
//console.log('GLOBAL OPTIONS ', api(sektionsLocalizedData.optNameForGlobalOptions)() );
var _groupSkope = self.getGroupSkopeForSiteTemplate(),
_hasSiteTemplateSet = false,
_inheritsSiteTemplate = false,
_globOptions = api(sektionsLocalizedData.optNameForGlobalOptions)();
var _is_inheritance_enabled_in_local_options = true,
currentSetValue = api( self.localSectionsSettingId() )(),
localOptions = currentSetValue.local_options;
if ( localOptions && _.isObject(localOptions) && localOptions.local_reset && !_.isUndefined( localOptions.local_reset.inherit_group_scope ) ) {
_is_inheritance_enabled_in_local_options = localOptions.local_reset.inherit_group_scope;
}
if ( _.isObject(_globOptions) && _globOptions.site_templates && _.isObject(_globOptions.site_templates ) ) {
_.each( _globOptions.site_templates, function( tmpl, siteTmplSkope ) {
// If no match found, keep sniffing
if ( !_hasSiteTemplateSet ) {
// How does this work ?
// the site_templates key are intended to match exactly the skope ids, as generated by NB skope system
// But there are exceptions for some skopes that have no "group skopes" and for which we've added the suffix "for_site_tmpl"
// ex : skp__404_for_site_tmpl, skp__date_for_site_tmpl, skp__search_for_site_tmpl @see php sek_get_module_params_for_sek_site_tmpl_pickers()
_hasSiteTemplateSet = _groupSkope === siteTmplSkope;
}
} );
}
_inheritsSiteTemplate = _hasSiteTemplateSet && !_hasLocalNBCustomizations && _is_inheritance_enabled_in_local_options;
var _msg = sektionsLocalizedData.i18n['This page is not customized with NB'];
if ( _inheritsSiteTemplate ) {
_msg = '' + sektionsLocalizedData.i18n['This page inherits a NB site template'] + '';
} else if ( _hasLocalNBCustomizations ) {
_msg = sektionsLocalizedData.i18n['This page is customized with NB'];
_msg += '';
}
$(self.topBarId).find('.sek-notifications')
.html([
'',
_msg
//sektionsLocalizedData.i18n['This page uses Nimble Builder template.']
].join(' '));
//.attr('data-doc-href', 'https://docs.presscustomizr.com/article/339-changing-the-page-template');
// if ( _.isObject( templateSettingValue ) && templateSettingValue.local_template && 'default' !== templateSettingValue.local_template ) {
// if ( _inheritsSiteTemplate ) {
// $(self.topBarId).find('.sek-notifications').addClass('is-linked').data('doc-href', 'https://docs.presscustomizr.com/article/428-how-to-use-site-templates-with-nimble-builder');
// } else {
// $(self.topBarId).find('.sek-notifications').removeClass('is-linked').data('doc-href','');
// }
// } else {
// $(self.topBarId).find('.sek-notifications').html('');
// }
};
api.bind('nimble-update-topbar-skope-status', printSektionsSkopeStatus );
var initOnSkopeReady = function() {
// Schedule notification rendering on init
// @see ::generateUIforLocalSkopeOptions()
api( self.localSectionsSettingId(), function( _localSectionsSetting_ ) {
// var localSectionsValue = _localSectionsSetting_(),
// initialLocalTemplateValue = ( _.isObject( localSectionsValue ) && localSectionsValue.local_options && localSectionsValue.local_options.template ) ? localSectionsValue.local_options.template : null;
// // on init
// printSektionsSkopeStatus( initialLocalTemplateValue );
printSektionsSkopeStatus({on_init : true});
});
// React to template changes
// @see ::generateUIforLocalSkopeOptions() for the declaration of self.getLocalSkopeOptionId() + '__template'
// api( self.getLocalSkopeOptionId() + '__template', function( _set_ ) {
// _set_.bind( function( to, from ) {
// printSektionsSkopeStatus( to );
// });
// });
};
// fire now
initOnSkopeReady();
// and on skope change, when user navigates through the previewed pages
// 'nimble-ready-for-current-skope' declared in ::initialize()
api.bind('nimble-ready-for-current-skope', function() {
initOnSkopeReady();
});
return $( self.topBarId );
}
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// Fired in ::initialize(), at api 'ready'
// March 2019 : history log tracks local and global section settings
// no tracking of the global option sektionsLocalizedData.optNameForGlobalOptions
initializeHistoryLogWhenSettingsRegistered : function() {
var self = this;
// This api.Value() is bound in ::setupTopBar
self.historyLog = new api.Value([{
status : 'current',
value : {
'local' : api( self.localSectionsSettingId() )(),//<= "nimble___[skp__post_page_10]"
'global' : api( self.getGlobalSectionsSettingId() )()
},
action : 'initial'
}]);
// LISTEN TO HISTORY LOG CHANGES AND UPDATE THE BUTTON STATE
self.historyLog.bind( function( newLog ) {
if ( _.isEmpty( newLog ) )
return;
var newCurrentKey = _.findKey( newLog, { status : 'current'} );
newCurrentKey = Number( newCurrentKey );
$( '#nimble-top-bar' ).find('[data-nimble-history]').each( function() {
if ( 'undo' === $(this).data('nimble-history') ) {
$(this).attr('data-nimble-state', 0 >= newCurrentKey ? 'disabled' : 'enabled');
} else {
$(this).attr('data-nimble-state', newLog.length <= ( newCurrentKey + 1 ) ? 'disabled' : 'enabled');
}
});
});
},
// React to a local or global setting change api( settingData.collectionSettingId )
// =>populates self.historyLog() observable value
// invoked in ::setupSettingsToBeSaved, if params.navigatingHistoryLogs !== true <=> not already navigating
trackHistoryLog : function( sektionSetInstance, params ) {
var self = this,
_isGlobal = sektionSetInstance.id === self.getGlobalSectionsSettingId();
// Safety checks
// trackHistoryLog must be invoked with a try catch statement
if ( !_.isObject( params ) || !_.isFunction( self.historyLog ) || !_.isArray( self.historyLog() ) ) {
api.errare( 'params, self.historyLog() ', params, self.historyLog() );
throw new Error('trackHistoryLog => invalid params or historyLog value');
}
// Always clean future values if the logs have been previously navigated back
var newHistoryLog = [],
historyLog = $.extend( true, [], self.historyLog() ),
sektionToRefresh;
if ( ! _.isEmpty( params.in_sektion ) ) {//<= module changed, column resized, removed...
sektionToRefresh = params.in_sektion;
} else if ( ! _.isEmpty( params.to_sektion ) ) {// column moved /
sektionToRefresh = params.to_sektion;
}
// Reset all status but 'future' to 'previous'
_.each( historyLog, function( log ) {
var newStatus = 'previous';
if ( 'future' == log.status )
return;
$.extend( log, { status : 'previous' } );
newHistoryLog.push( log );
});
newHistoryLog.push({
status : 'current',
value : _isGlobal ? { global : sektionSetInstance() } : { local : sektionSetInstance() },
action : _.isObject( params ) ? ( params.action || '' ) : '',
sektionToRefresh : sektionToRefresh
});
self.historyLog( newHistoryLog );
},
// @param direction = string 'undo', 'redo'
// @return void()
// Fired on click in the topbar or when hitting ctrl z / y
navigateHistory : function( direction ) {
var self = this,
historyLog = $.extend( true, [], self.historyLog() );
// log model
// {
// status : 'current', 'previous', 'future'
// value : {},
// action : 'sek-add-column'
// }
// UPDATE THE SETTING VALUE
var previous,
current,
future,
newHistoryLog = [],
newSettingValue,
previousSektionToRefresh,
currentSektionToRefresh;
_.each( historyLog, function( log ) {
if ( ! _.isEmpty( newSettingValue ) ) {
return;
}
switch( log.status ) {
case 'previous' :
previous = log;
break;
case 'current' :
current = log;
break;
case 'future' :
future = log;
break;
}
switch( direction ) {
case 'undo' :
// the last previous is our new setting value
if ( ! _.isEmpty( current ) && ! _.isEmpty( previous ) ) {
newSettingValue = previous.value;
previousSektionToRefresh = current.sektionToRefresh;
currentSektionToRefresh = previous.sektionToRefresh;
}
break;
case 'redo' :
// the first future is our new setting value
if ( ! _.isEmpty( future ) ) {
newSettingValue = future.value;
previousSektionToRefresh = current.sektionToRefresh;
currentSektionToRefresh = future.sektionToRefresh;
}
break;
}
});
// set the new setting Value
if( !_.isUndefined( newSettingValue ) ) {
if ( !_.isEmpty( newSettingValue.local ) ) {
api( self.localSectionsSettingId() )( self.validateSettingValue( newSettingValue.local, 'local' ), { navigatingHistoryLogs : true } );
// Clean and regenerate the local option setting
// Note that we also do it after a local import, tmpl injection or local reset
//
// Settings are normally registered once and never cleaned, unlike controls.
// Updating the setting value will refresh the sections
// but the local options, persisted in separate settings, won't be updated if the settings are not cleaned
// Example of local setting id :
// __nimble__skp__post_page_2__localSkopeOptions__template
// or
// __nimble__skp__home__localSkopeOptions__custom_css
api.czr_sektions.generateUI({
action : 'sek-generate-local-skope-options-ui',
clean_settings_and_controls_first : true//<= see api.czr_sektions.generateUIforLocalSkopeOptions()
});
}
if ( !_.isEmpty( newSettingValue.global ) ) {
api( self.getGlobalSectionsSettingId() )( self.validateSettingValue( newSettingValue.global, 'global' ), { navigatingHistoryLogs : true } );
}
// If the information is available, refresh only the relevant sections
// otherwise fallback on a full refresh
var previewHasBeenRefreshed = false;
// if ( ! _.isEmpty( previousSektionToRefresh ) ) {
// api.previewer.trigger( 'sek-refresh-level', {
// level : 'section',
// id : previousSektionToRefresh
// });
// } else {
// api.previewer.refresh();
// previewHasBeenRefreshed = true;
// }
// if ( currentSektionToRefresh != previousSektionToRefresh ) {
// if ( ! _.isEmpty( currentSektionToRefresh ) ) {
// api.previewer.trigger( 'sek-refresh-level', {
// level : 'section',
// id : currentSektionToRefresh
// });
// } else if ( ! previewHasBeenRefreshed ) {
// api.previewer.refresh();
// }
// }
api.previewer.refresh();
// Always make sure that the ui gets refreshed
api.previewer.trigger( 'sek-pick-content', {});
// Clean registered control
self.cleanRegisteredAndLargeSelectInput();//<= normal cleaning
// Clean even the level settings
// => otherwise the level settings won't be synchronized when regenerating their ui.
self.cleanRegisteredLevelSettings();// setting cleaning
}
// UPDATE THE HISTORY LOG
var currentKey = _.findKey( historyLog, { status : 'current'} );
currentKey = Number( currentKey );
if ( ! _.isNumber( currentKey ) ) {
api.errare( 'Error when navigating the history log, the current key should be a number');
return;
}
_.each( historyLog, function( log, key ) {
newLog = $.extend( true, {}, log );
// cast keys to number so we can compare them
key = Number( key );
switch( direction ) {
case 'undo' :
if ( 0 < currentKey ) {
if ( key === ( currentKey - 1 ) ) {
newLog.status = 'current';
} else if ( key === currentKey ) {
newLog.status = 'future';
}
}
break;
case 'redo' :
if ( historyLog.length > ( currentKey + 1 ) ) {
if ( key === currentKey ) {
newLog.status = 'previous';
} else if ( key === ( currentKey + 1 ) ) {
newLog.status = 'current';
}
}
break;
}
newHistoryLog.push( newLog );
});
self.historyLog( newHistoryLog );
}
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// fired in ::setupTopBar(), at api.bind( 'ready', function() {})
setupLevelTree : function() {
var self = this, stringifiedLTVal;
self.levelTree = new api.Value([]);
self.levelTree.bind( function( val ) {
// Refresh when the collection is being modified from the tree
if ( self.levelTreeExpanded() ) {
self.renderOrRefreshTree();
}
});
// March 2021 => highlight level tree button in blue if NB levels already inserted.
var maybeHighlightCtrlButton = function( val ) {
try { stringifiedLTVal = JSON.stringify( val ); } catch(er) {
api.errorLog('::setupLevelTree => error when JSON.stringify Level Tree');
}
if ( !_.isString( stringifiedLTVal ) )
return;
// look for a NB level id starting looking like __nimble__986b1c3921fe
if ( -1 !== stringifiedLTVal.indexOf('__nimble__') ) {
$('.sek-level-tree button', self.topBarId).css('color', '#46d2ff' );
} else {
$('.sek-level-tree button', self.topBarId).css('color', '' );
}
};
self.levelTree.bind( _.debounce( function(val) {
maybeHighlightCtrlButton( val );
}, 1000 ));
// Initial Button state based on the current tree value
$('#customize-preview').one('nimble-top-bar-rendered', function() {
maybeHighlightCtrlButton( self.setLevelTreeValue() );
});
// SETUP AND REACT TO LEVEL TREE EXPANSION
self.levelTreeExpanded = new api.Value(false);
self.levelTreeExpanded.bind( function(expanded) {
self.cachedElements.$body.toggleClass( 'sek-level-tree-expanded', expanded );
if ( expanded ) {
// Close template gallery, template saver, section saver
self.templateGalleryExpanded(false);
self.tmplDialogVisible(false);
if ( self.saveSectionDialogVisible ) {
self.saveSectionDialogVisible(false);
}
// Set the level tree now
self.setLevelTreeValue();
// Make sure we the tree is set first
if ( _.isEmpty( self.levelTree() ) ) {
api.previewer.trigger('sek-notify', {
type : 'info',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['No sections to navigate'] + '',
''
].join('')
});
// self disable
self.levelTreeExpanded(false);
return;
}
$('#customize-preview iframe').css('z-index', 1);
self.renderOrRefreshTree();
} else if ( $('#nimble-level-tree').length > 0 ) {
_.delay( function() {
$('#nimble-level-tree').remove();
$('#customize-preview iframe').css('z-index', '');
}, 300 );
}
});
// REFRESH THE TREE WHEN THE ACTIVE LOCATIONS CHANGE
// @see ::initialize to understand how active locations are updated
self.activeLocations.bind(function() {
if ( !_.isEmpty( self.levelTree() ) ) {
self.renderOrRefreshTree();
}
});
// API READY
api.previewer.bind('ready', function() {
// LEVEL TREE
// on each skope change
// - set the level tree
// - bind the local and global settings so that they refresh the level tree when changed
self.localSectionsSettingId.callbacks.add( function() {
self.levelTreeExpanded(false);
// Bind the global and local settings if not bound yet
_.each( [ self.getGlobalSectionsSettingId(), self.localSectionsSettingId(), sektionsLocalizedData.optNameForGlobalOptions ], function( setId ){
if ( api(setId)._isBoundForNimbleLevelTree )
return;
api(setId).bind( function(to) {
self.setLevelTreeValue();
});
api(setId)._isBoundForNimbleLevelTree = true;
});
});
});
// SETUP CLICK EVENTS IN THE TREE
self.cachedElements.$body.on('click', '#nimble-level-tree [data-nimb-level]', function(evt) {
evt.preventDefault();
evt.stopPropagation();
var $el = $(evt.target),
$closestLevel = $el.closest('[data-nimb-level]');
api.previewer.send('sek-animate-to-level', { id : $closestLevel.data('nimb-id') });
api.previewer.send('sek-clean-level-uis');
// Display the level ui in the preview
// and expand the level options in the customizer control panel
_.delay( function() {
api.previewer.send('sek-display-level-ui', { id : $closestLevel.data('nimb-id') });
var _id = $closestLevel.data('nimb-id'),
_level = $closestLevel.data('nimb-level');
if ( 'column' === _level || 'section' === _level ) {
api.previewer.trigger('sek-edit-options', { id : _id, level : _level });
} else if ( 'module' === _level ) {
api.previewer.trigger('sek-edit-module', { id : _id, level : _level });
}
}, 100 );
});
self.cachedElements.$body.on('click', '#nimble-level-tree .sek-remove-level', function(evt) {
evt.preventDefault();
evt.stopPropagation();
var $el = $(evt.target).closest('[data-nimb-level]');
api.previewer.trigger('sek-remove', {
level : $el.data('nimb-level'),
id : $el.data('nimb-id'),
location : $el.closest('[data-nimb-level="location"]').data('nimb-id'),
in_sektion : $el.closest('[data-nimb-level="section"]').data('nimb-id'),
in_column : $el.closest('[data-nimb-level="column"]').data('nimb-id')
});
$el.fadeOut('slow');
// Refresh
self.renderOrRefreshTree();
});
// Collapse tree ( also possible by clicking on the tree icon in the top Nimble bar )
self.cachedElements.$body.on('click', '.sek-close-level-tree' , function(evt) {
evt.preventDefault();
self.levelTreeExpanded(false);
});
},
// This method updates the levelTree observable api.Value()
setLevelTreeValue : function() {
var self = this,
globalCollSetId = self.getGlobalSectionsSettingId(),
localCollSetId = self.localSectionsSettingId(),
globalOptionSetId = sektionsLocalizedData.optNameForGlobalOptions,
globalColSetValue, localColSetValue,
globalCollection, localCollection,
rawGlobalOptionsValue,
missingDependantSettingId = false;
// Check if all dependant settings are registered
// we won't go further if any of the 3 setting id's is not yet registered
_.each( [globalCollSetId, localCollSetId, globalOptionSetId ], function( setId ) {
if ( !api.has(setId) ) {
missingDependantSettingId = setId;
return;
}
});
if ( false !== missingDependantSettingId ) {
api.errare( '::setLevelTreeValue => a setting id is not registered ');
return;
}
// Normalizes the setting values
globalColSetValue = api(globalCollSetId)();
globalCollection = _.isObject( globalColSetValue ) ? $.extend( true, {}, globalColSetValue ) : {};
globalCollection = ! _.isEmpty( globalCollection.collection )? globalCollection.collection : [];
globalCollection = _.isArray( globalCollection ) ? globalCollection : [];
localColSetValue = api(localCollSetId)();
localColSetValue = _.isObject( localColSetValue ) ? localColSetValue : {};
localCollection = $.extend( true, {}, localColSetValue );
localCollection = ! _.isEmpty( localCollection.collection ) ? localCollection.collection : [];
localCollection = _.isArray( localCollection ) ? localCollection : [];
var raw_col = _.union( globalCollection, localCollection ),
local_header_footer_value,
global_header_footer_value,
has_local_header_footer = false,
has_global_header_footer = false;
rawGlobalOptionsValue = api( globalOptionSetId )();
rawGlobalOptionsValue = _.isObject( rawGlobalOptionsValue ) ? rawGlobalOptionsValue : {};
// HEADER-FOOTER => do we have a header-footer set, local or global ?
// LOCAL
if ( localColSetValue.local_options && localColSetValue.local_options.local_header_footer ) {
local_header_footer_value = localColSetValue.local_options.local_header_footer['header-footer'];
has_local_header_footer = 'nimble_local' === local_header_footer_value;
}
// GLOBAL
// there can be a global header footer if
// 1) local is not set to 'nimble_local' or 'theme'
// and
// 2) the global option is set to 'nimble_global'
//
// OR when
// 1) local is set to 'nimble_global'
if ( rawGlobalOptionsValue.global_header_footer && !has_local_header_footer && 'theme' !== local_header_footer_value) {
global_header_footer_value = rawGlobalOptionsValue.global_header_footer['header-footer'];
has_global_header_footer = 'nimble_global' === global_header_footer_value || 'nimble_global' === local_header_footer_value;
}
var filteredCollection = $.extend( true, [], raw_col ),
header_loc,
footer_loc;
filteredCollection = _.filter( filteredCollection, function( loc, key ) {
return !_.contains( ['nimble_global_header', 'nimble_global_footer', 'nimble_local_header', 'nimble_local_footer'], loc.id );
});
// RE-ORGANIZE LOCATIONS SO THAT WE HAVE
// - header
// - content loc #1
// - content loc #2
// - ...
// - footer
var wrapContentLocationWithHeaderFoooterLocations = function( scope ) {
header_loc = _.findWhere(raw_col, {id:'nimble_' + scope + '_header'});
footer_loc = _.findWhere(raw_col, {id:'nimble_' + scope + '_footer'});
filteredCollection.unshift(header_loc);
filteredCollection.push(footer_loc);
};
if ( has_local_header_footer ) {
wrapContentLocationWithHeaderFoooterLocations('local');
} else if ( has_global_header_footer ) {
wrapContentLocationWithHeaderFoooterLocations('global');
}
// RE-ORDER LOCATIONS IN THE SAME ORDER AS THEY ARE IN THE DOM
// @see ::initialize to understand how active locations are updated
var contextuallyActiveLocactions = self.activeLocations(),
orderedCollection = [],
candidate;
if ( !_.isEmpty(contextuallyActiveLocactions) ) {
_.each( contextuallyActiveLocactions, function( loc ) {
candidate = _.findWhere(filteredCollection, {id:loc});
if( !_.isUndefined(candidate) ) {
orderedCollection.push(candidate);
}
});
} else {
orderedCollection = filteredCollection;
}
// Store it now
self.levelTree( orderedCollection );
return orderedCollection;
},
// print the tree
renderOrRefreshTree : function() {
var self = this,
_tmpl;
if( $('#nimble-level-tree').length < 1 ) {
// RENDER
try {
_tmpl = wp.template( 'nimble-level-tree' )( {} );
} catch( er ) {
api.errare( 'Error when parsing the nimble-level-tree template', er );
return false;
}
$( '#customize-preview' ).after( $( _tmpl ) );
}
$('#nimble-level-tree').find('.sek-tree-wrap').html( self.getLevelTreeHtml() );
},
// recursive helper
// return an html string describing the contextually printed sections
getLevelTreeHtml : function( _col, level ) {
var self = this;
_col = _col || self.levelTree();
var levelType,
levelName,
_html,
skipLevel = false;
if ( !_.isArray( _col ) || _.isEmpty( _col ) ) {
api.errare('::buildLevelTree => invalid collection param', _col );
return _html;
}
var remove_icon_html = 'delete_forever';
_html = '
';
_.each( _col, function( _level_param ) {
if ( _.isUndefined( _level_param.level ) ){
api.errare('::buildLevelTree => missing level property', _level_param );
return;
}
if ( _.isUndefined( _level_param.id ) ){
api.errare('::buildLevelTree => missing id property', _level_param );
return;
}
// Set some vars now
levelType = _level_param.level;
levelName = levelType;
// if the level is a location, is this location contextually active ?
// @see ::initialize to understand how active locations are updated
if ( 'location' === levelType ) {
skipLevel = !_.contains( self.activeLocations(), _level_param.id );
}
if ( !skipLevel ) {
//try to get the i18n level name, fall back on the level type
if ( sektionsLocalizedData.i18n[levelType] ) {
levelName = sektionsLocalizedData.i18n[levelType];
}
if ( true === _level_param.is_nested ) {
levelName = sektionsLocalizedData.i18n['nested section'];
}
remove_icon_html = 'location' !== levelType ? remove_icon_html : '';
_html += '
';
_html += '
';
// add module type and icon
if ( 'module' === levelType ) {
_html += [
self.getTreeModuleIcon( _level_param.module_type ),
self.getTreeModuleTitle( _level_param.module_type )
].join(' ');
}
// add the rest of the html, common to all elements
_html += [
' ',
levelName,
'( id :',
_level_param.id,
')',
remove_icon_html
].join(' ');
_html += '
';
return _html;
},
// the module icons can be
// an svg file like Nimble__divider_icon.svg => in this case we build and return the full url
// or a font_icon like ''
getTreeModuleIcon : function( modType ) {
var _icon = {}, icon_img_src;
_.each( sektionsLocalizedData.moduleCollection, function( modData ) {
if ( !_.isEmpty( _icon ) )
return;
if ( modType === modData['content-id'] ) {
if ( !_.isEmpty( modData.icon ) ) {
if ( 'http' === modData.icon.substring(0, 4) ) {
icon_img_src = modData.icon;
} else {
icon_img_src = sektionsLocalizedData.moduleIconPath + modData.icon;
}
_icon = {
svg : modData.icon ? icon_img_src : '',
font : modData.font_icon ? modData.font_icon : ''
};
}
}
});
if ( !_.isEmpty( _icon.svg ) ) {
return '';
} else if ( !_.isEmpty( _icon.font ) ) {
return _icon.font;
}
},
getTreeModuleTitle : function( modType ) {
var _title = {};
_.each( sektionsLocalizedData.moduleCollection, function( modData ) {
if ( !_.isEmpty( _title ) )
return;
if ( modType === modData['content-id'] ) {
_title = modData.title;
}
});
return _title;
}
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
//
//Note : idOfSectionToSave is set when user clicks on the save icon, and is reset when saving/updating closing save dialog
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// SAVE SECTION DIALOG BLOCK
// fired in ::initialize()
setupSaveSectionUI : function() {
var self = this;
// Declare api values and schedule reactions
self.saveSectionDialogVisible = new api.Value( false );// Hidden by default
// The observer for visibility
// connected to other observers
self.saveSectionDialogVisible.bind( function( visible ){
if ( visible ) {
// close template gallery
// close level tree
self.templateGalleryExpanded(false);
self.levelTreeExpanded(false);
if ( self.tmplDialogVisible ) {
self.tmplDialogVisible(false);
}
}
self.toggleSaveSectionUI(visible);
});
// Will store the collection of saved sections
self.allSavedSections = new api.Value('_not_populated_');
// When the collection is refreshed
// - populate select options
// - set the select value to default 'none'
self.allSavedSections.bind( function( sec_collection ) {
if ( !_.isObject(sec_collection) ) {
api.errare('error setupSaveSectionUI => section collection should be an object');
return;
}
sec_collection = _.isEmpty(sec_collection) ? {} : sec_collection;
self.refreshSectionPickerHtml( sec_collection );
});
self.saveSectionDialogMode = new api.Value('hidden');// 'save' default mode is set when dialog html is rendered
self.saveSectionDialogMode.bind( function(mode){
if ( !_.contains(['hidden', 'save', 'update', 'remove', 'edit' ], mode ) ) {
api.errare('error setupSaveSectionUI => unknown section dialog mode', mode );
mode = 'save';
}
// Set the button pressed state
var $secSaveDialogWrap = $('#nimble-top-section-save-ui'),
$titleInput = $secSaveDialogWrap.find('#sek-saved-section-title'),
$descInput = $secSaveDialogWrap.find('#sek-saved-section-description');
$secSaveDialogWrap.find('[data-section-mode-switcher]').attr('aria-pressed', false );
$secSaveDialogWrap.find('[data-section-mode-switcher="' + mode +'"]').attr('aria-pressed', true );
// update the current mode
$('#nimble-top-section-save-ui').attr('data-sek-section-dialog-mode', mode );
// make sure the remove dialog is hidden
$secSaveDialogWrap.removeClass('sek-removal-confirmation-opened');
var $selectEl;
// execute actions depending on the selected mode
switch( mode ) {
case 'save' :
// When selecting 'save', make sure the title and description input are cleaned
$titleInput.val('');
$descInput.val('');
break;
case 'update' :
case 'edit' :
$selectEl = $secSaveDialogWrap.find('.sek-saved-section-picker');
// Make sure the select value is always reset when switching mode
$selectEl.val('none').trigger('change');
self.setSavedSectionCollection().done( function( sec_collection ) {
// refresh section picker in case the user updated without changing anything
self.refreshSectionPickerHtml();
$selectEl.val( self.userSectionToEdit || 'none' ).trigger('change');
self.userSectionToEdit = null;
});
break;
case 'remove' :
console.log('sOOO ?', self.userSectionToRemove );
$selectEl = $secSaveDialogWrap.find('.sek-saved-section-picker');
// Make sure the select value is always reset when switching mode
$selectEl.val('none').trigger('change');
self.setSavedSectionCollection().done( function( sec_collection ) {
// refresh section picker in case the user updated without changing anything
self.refreshSectionPickerHtml();
$selectEl.val( self.userSectionToRemove || 'none' ).trigger('change');
self.userSectionToRemove = null;
});
break;
}//switch
// when user clicks on the remove icon of a section in the collection
// => hide save and update buttons
if ( 'remove' === mode && _.isEmpty( self.idOfSectionToSave ) ) {
$secSaveDialogWrap.addClass('sek-is-removal-only');
} else {
$secSaveDialogWrap.removeClass('sek-is-removal-only');
}
});//self.saveSectionDialogMode.bind()
},
///////////////////////////////////////////////
///// RENDER DIALOG BOX AND SCHEDULE CLICK ACTIONS
refreshSectionPickerHtml : function( sec_collection ) {
sec_collection = sec_collection || this.allSavedSections();
var $secSaveDialogWrap = $('#nimble-top-section-save-ui'),
$selectEl = $secSaveDialogWrap.find('.sek-saved-section-picker');
// Make sure the select value is always reset when switching mode
$selectEl.val('none').trigger('change');
// empty all options but the default 'none' one
$selectEl.find('option').each( function() {
if ( 'none' !== $(this).attr('value') ) {
$(this).remove();
}
});
// Make sure we don't populate the collection twice ( if user clicks two times fast )
// if ( $secSaveDialogWrap.hasClass('sec-collection-populated') )
// return;
var _default_title = 'section title not set',
_title,
_last_modified_date,
_html = '';
_.each( sec_collection, function( _sec_data, _sec_post_name ) {
if ( !_.isObject(_sec_data) )
return;
_last_modified_date = _sec_data.last_modified_date ? _sec_data.last_modified_date : '';
_title = _sec_data.title ? _sec_data.title : _default_title;
_html +='';
});
$selectEl.append(_html);
// flag so we know it's done
// => controls the CSS visibility of the select element
$secSaveDialogWrap.addClass('section-collection-populated');
},
//@param = { }
renderSectionSaveUI : function( params ) {
if ( $('#nimble-top-section-save-ui').length > 0 )
return $('#nimble-top-section-save-ui');
var self = this;
try {
_tmpl = wp.template( 'nimble-top-section-save-ui' )( {} );
} catch( er ) {
api.errare( 'Error when parsing nimble-top-section-save-ui template', er );
return false;
}
$('#customize-preview').after( $( _tmpl ) );
return $('#nimble-top-section-save-ui');
},
///////////////////////////////////////////////
///// DOM EVENTS
// Fired once, on first rendering
maybeScheduleSectionSaveDOMEvents : function() {
var self = this, $secSaveDialogWrap = $('#nimble-top-section-save-ui');
if ( $secSaveDialogWrap.data('nimble-sec-save-dom-events-scheduled') )
return;
// ATTACH DOM EVENTS
// Dialog Mode Switcher
$secSaveDialogWrap
.on( 'click', '[data-section-mode-switcher]', function(evt) {
evt.preventDefault();
self.saveSectionDialogMode($(this).data('section-mode-switcher'));
})
// React to section select
// update title and description fields on section selection
.on( 'change', '.sek-saved-section-picker', function(evt){ self.reactOnSectionSelection(evt, $(this) ); })
// SAVE
.on( 'click', '.sek-do-save-section', function(evt){
$secSaveDialogWrap.addClass('nimble-section-processing-ajax');
self.saveOrUpdateSavedSection(evt).done( function( response ) {
$secSaveDialogWrap.removeClass('nimble-section-processing-ajax');
if ( response.success ) {
self.saveSectionDialogVisible( false );
self.setSavedSectionCollection( { refresh : true } );// <= true for refresh
}
});
})
// UPDATE
.on( 'click', '.sek-do-update-section', function(evt){
var $selectEl = $secSaveDialogWrap.find('.sek-saved-section-picker'),
sectionPostNameCandidateForUpdate = $selectEl.val();
// make sure we don't try to remove the default option
if ( 'none' === sectionPostNameCandidateForUpdate || _.isEmpty(sectionPostNameCandidateForUpdate) )
return;
$secSaveDialogWrap.addClass('nimble-section-processing-ajax');
self.saveOrUpdateSavedSection(evt, sectionPostNameCandidateForUpdate).done( function(response) {
$secSaveDialogWrap.removeClass('nimble-section-processing-ajax');
if ( response.success ) {
self.saveSectionDialogVisible( false );
self.setSavedSectionCollection( { refresh : true } )// <= true for refresh
.done( function( sec_collection ) {
// refresh section picker in case the user updated without changing anything
self.refreshSectionPickerHtml();
});
}
});
})
// REMOVE
// Reveal remove dialog
.on( 'click', '.sek-open-remove-confirmation', function(evt){
$secSaveDialogWrap.addClass('sek-removal-confirmation-opened');
})
// Do Remove
.on( 'click', '.sek-do-remove-section', function(evt){
var $selectEl = $secSaveDialogWrap.find('.sek-saved-section-picker'),
sectionPostNameCandidateForRemoval = $selectEl.val();
// make sure we don't try to remove the default option
if ( 'none' === sectionPostNameCandidateForRemoval || _.isEmpty(sectionPostNameCandidateForRemoval) )
return;
$secSaveDialogWrap.addClass('nimble-section-processing-ajax');
self.removeSavedSection(evt, sectionPostNameCandidateForRemoval).done( function(response) {
$secSaveDialogWrap.removeClass('nimble-section-processing-ajax');
$secSaveDialogWrap.removeClass('sek-removal-confirmation-opened');
if ( response.success ) {
self.setSavedSectionCollection( { refresh : true } );// <= true for refresh
}
});
})
// Cancel Remove
.on( 'click', '.sek-cancel-remove-section', function(evt){
$secSaveDialogWrap.removeClass('sek-removal-confirmation-opened');
});
// Switch to update mode
//$secSaveDialogWrap.on( 'click', '[data-section-mode-switcher="update"]', function(evt){ });
$('.sek-close-dialog', $secSaveDialogWrap ).on( 'click', function(evt) {
evt.preventDefault();
self.saveSectionDialogVisible(false);
});
// Say we're done with DOM event scheduling
$secSaveDialogWrap.data('nimble-sec-save-dom-events-scheduled', true );
},
// Is used in update and remove modes
reactOnSectionSelection : function(evt, $selectEl ){
var self = this,
$secSaveDialogWrap = $('#nimble-top-section-save-ui'),
_sectionPostName = $selectEl.val(),
$titleInput = $secSaveDialogWrap.find('#sek-saved-section-title'),
$descInput = $secSaveDialogWrap.find('#sek-saved-section-description'),
// The informative class control the visibility of the title and the description in CSS
_informativeClass = 'update' === self.saveSectionDialogMode() ? 'sek-section-update-selected' : 'sek-section-remove-selected';
if ( 'none' === _sectionPostName ) {
$titleInput.val('');
$descInput.val('');
$secSaveDialogWrap.removeClass(_informativeClass);
} else {
var _allSavedSections = self.allSavedSections();
var _selectedSection = _sectionPostName;
// normalize
_allSavedSections = ( _.isObject(_allSavedSections) && !_.isArray(_allSavedSections) ) ? _allSavedSections : {};
_allSavedSections[_sectionPostName] = $.extend( {
title : '',
description : '',
last_modified_date : ''
}, _allSavedSections[_sectionPostName] || {} );
$titleInput.val( _allSavedSections[_sectionPostName].title );
$descInput.val( _allSavedSections[_sectionPostName].description );
$secSaveDialogWrap.addClass(_informativeClass);
}
},
///////////////////////////////////////////////
///// AJAX ACTIONS
// Fired on 'click' on .sek-do-save-section btn
// @param sectionPostNameCandidateForUpdate is only provided when saving
saveOrUpdateSavedSection : function(evt, sectionPostNameCandidateForUpdate ) {
var self = this,
_dfd_ = $.Deferred(),
_isEditSectionMode = 'edit' === self.saveSectionDialogMode();
// idOfSectionToSave is set when reacting to click action
// @see react to preview 'sek-toggle-save-section-ui'
if ( !_isEditSectionMode ) {
if ( !self.idOfSectionToSave || _.isEmpty( self.idOfSectionToSave ) ) {
api.errare('saveOrUpdateSavedSection => error => missing section id');
return _dfd_.resolve( {success:false});
}
}
evt.preventDefault();
var $_title = $('#sek-saved-section-title'),
section_title = $_title.val(),
section_description = $('#sek-saved-section-description').val(),
sectionModel;
// Only get the section model when not in edit section mode
if ( !_isEditSectionMode ) {
sectionModel = $.extend( true, {}, self.getLevelModel( self.idOfSectionToSave ) );
if ( 'no_match' == sectionModel ) {
api.errare('saveOrUpdateSavedSection => error => no section model with id ' + self.idOfSectionToSave );
return _dfd_.resolve( {success:false});
}
// Do some pre-processing before ajaxing
// Note : ids will be replaced server side
sectionModel = self.preProcessSection( sectionModel );
if ( !_.isObject( sectionModel ) ) {
api.errare('::saveOrUpdateSavedSection => error => invalid sectionModel');
return _dfd_.resolve( {success:false});
}
}
if ( _.isEmpty( section_title ) ) {
$_title.addClass('error');
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['A title is required'] + '',
''
].join('')
});
return _dfd_.resolve( {success:false});
}
$('#sek-saved-section-title').removeClass('error');
wp.ajax.post( 'sek_save_user_section', {
nonce: api.settings.nonce.save,
section_data: _isEditSectionMode ? '' : JSON.stringify( sectionModel ),
// the following will be saved in 'metas'
section_title: section_title,
section_description: section_description,
section_post_name: sectionPostNameCandidateForUpdate || '',// <= provided when updating a section
skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' ),
edit_metas_only: _isEditSectionMode ? 'yes' : 'no'//<= in this case we only update title and description. Not the template content
//active_locations : api.czr_sektions.activeLocations()
})
.done( function( response ) {
//console.log('SAVED POST ID', response );
_dfd_.resolve( {success:true});
// response is {section_post_id: 436}
api.previewer.trigger('sek-notify', {
type : 'success',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Template saved'] + '',
''
].join('')
});
})
.fail( function( er ) {
_dfd_.resolve( {success:false});
api.errorLog( 'ajax sek_save_section => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Error when processing template'] + '',
''
].join('')
});
})
.always( function() {
// reset the id of section to save
// => because we need to know when we are in 'remove' mode when user clicked on remove icon in the section collection, => which hides save and update buttons
self.idOfSectionToSave = null;
});
return _dfd_;
},//saveOrUpdateSavedSection
// @return a section model
// Note : ids are reset server side
// Example of section model before preprocessing
// {
// collection: [{…}]
// id: "" //<= reset server side
// level: "section"
// is_nested : false
// options: {bg: {…}}
// ver_ini: "1.1.8"
// }
preProcessSection : function( sectionModel ) {
if ( !_.isObject( sectionModel ) ) {
return null;
}
var preprocessedModel = $.extend( {}, true, sectionModel );
// Make sure a nested section is saved as normal
if ( _.has( preprocessedModel, 'is_nested') ) {
preprocessedModel = _.omit( preprocessedModel, 'is_nested' );
}
return preprocessedModel;
},
// Fired on 'click on .sek-do-remove-section btn
removeSavedSection : function(evt, sectionPostNameCandidateForRemoval ) {
var self = this, _dfd_ = $.Deferred();
evt.preventDefault();
wp.ajax.post( 'sek_remove_user_section', {
nonce: api.settings.nonce.save,
section_post_name: sectionPostNameCandidateForRemoval
//skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' )
})
.done( function( response ) {
_dfd_.resolve( {success:true});
// response is {section_post_id: 436}
api.previewer.trigger('sek-notify', {
type : 'success',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Template removed'] + '',
''
].join('')
});
})
.fail( function( er ) {
_dfd_.resolve( {success:false});
api.errorLog( 'ajax sek_remove_section => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Error when processing templates'] + '',
''
].join('')
});
})
.always( function() {
// reset the id of section to save
// => because we need to know when we are in 'remove' mode when user clicked on remove icon in the section collection, => which hides save and update buttons
self.idOfSectionToSave = null;
});
return _dfd_;
},
///////////////////////////////////////////////
///// REVEAL / HIDE DIALOG BOX
/// react on self.saveSectionDialogVisible.bind(...)
// @return void()
// self.saveSectionDialogVisible.bind( function( visible ){
// self.toggleSaveSectionUI( visible );
// });
toggleSaveSectionUI : function( visible ) {
visible = _.isUndefined( visible ) ? true : visible;
var self = this,
_renderAndSetup = function() {
$.when( self.renderSectionSaveUI({}) ).done( function( $_el ) {
self.maybeScheduleSectionSaveDOMEvents();//<= schedule on the first display only
self.saveUIContainer = $_el;
//display
_.delay( function() {
// set dialog mode now so we display the relevant fields on init
self.saveSectionDialogMode('save');// Default mode is save
self.cachedElements.$body.addClass('sek-save-section-ui-visible');
}, 200 );
// set section id input value
//$('#sek-saved-section-id').val( sectionId );
});
},
_hide = function() {
var dfd = $.Deferred();
self.cachedElements.$body.removeClass('sek-save-section-ui-visible');
if ( $( '#nimble-top-section-save-ui' ).length > 0 ) {
//remove Dom element after slide up
_.delay( function() {
// set dialog mode back to 'hidden' mode
self.saveSectionDialogMode = self.saveSectionDialogMode ? self.saveSectionDialogMode : new api.Value();
self.saveSectionDialogMode('hidden');
self.saveUIContainer.remove();
// reset the id of section to save
// => because we need to know when we are in 'remove' mode when user clicked on remove icon in the section collection, => which hides save and update buttons
self.idOfSectionToSave = null;
dfd.resolve();
}, 250 );
} else {
dfd.resolve();
}
return dfd.promise();
};
if ( visible ) {
_renderAndSetup();
} else {
_hide().done( function() {
self.saveSectionDialogVisible( false );//should be already false
});
}
},
///////////////////////////////////////////////
///// TMPL COLLECTION
// @return $.promise
// @param params = {refresh : false};
setSavedSectionCollection : function( params ) {
var self = this, _dfd_ = $.Deferred();
// refresh is true on save, update, remove success
params = params || {refresh : false};
// If the collection is already set, return it.
// unless this is a "refresh" case
if ( !params.refresh && '_not_populated_' !== self.allSavedSections() ) {
return _dfd_.resolve( self.allSavedSections() );
}
var _promise;
// Prevent a double request while ajax request is being processed
if ( self.sectionCollectionPromise && 'pending' === self.sectionCollectionPromise.state() ) {
_promise = self.sectionCollectionPromise;
} else {
_promise = self.getSavedSectionCollection( params );
}
_promise.done( function( sec_collection ) {
self.allSavedSections( sec_collection );
_dfd_.resolve( sec_collection );
});
return _dfd_.promise();
},
// @return a promise
// @param params = {refresh : false};
// also used from the input Constructor of sek_my_sections_sec_picker_module
// @param params = {refresh : false};
getSavedSectionCollection : function( params ) {
var self = this;
// refresh is true on save, update, remove success
params = params || {refresh : false};
self.sectionCollectionPromise = $.Deferred();
if ( !params.refresh && '_not_populated_' !== self.allSavedSections() ) {
self.sectionCollectionPromise.resolve( self.allSavedSections() );
return self.sectionCollectionPromise;
}
wp.ajax.post( 'sek_get_all_saved_sections', {
nonce: api.settings.nonce.save
//skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' )
})
.done( function( sec_collection ) {
if ( _.isObject(sec_collection) && !_.isArray( sec_collection ) ) {
self.sectionCollectionPromise.resolve( sec_collection );
} else {
self.sectionCollectionPromise.resolve( {} );
if ( !_.isEmpty( sec_collection ) ) {
api.errorLog('control::getSavedSectionCollection => collection is empty or invalid');
}
}
// response is {section_post_id: 436}
//self.saveSectionDialogVisible( false );
// api.previewer.trigger('sek-notify', {
// type : 'success',
// duration : 10000,
// message : [
// '',
// 'Your section has been saved.',
// ''
// ].join('')
// });
})
.fail( function( er ) {
api.errorLog( 'ajax sek_get_all_saved_section => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Error when processing templates'] + '',
''
].join('')
});
self.sectionCollectionPromise.resolve({});
});
return self.sectionCollectionPromise;
}
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
// introduced in april 2020 for https://github.com/presscustomizr/nimble-builder/issues/655
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// SAVE TMPL DIALOG BLOCK
// fired in ::initialize()
setupSaveTmplUI : function() {
var self = this;
// Declare api values and schedule reactions
self.tmplDialogVisible = new api.Value( false );// Hidden by default
if ( !sektionsLocalizedData.isTemplateSaveEnabled ) {
return;
}
self.tmplDialogVisible.bind( function( visible ){
if ( visible ) {
// close template gallery
// close level tree
self.templateGalleryExpanded(false);
self.levelTreeExpanded(false);
if ( self.saveSectionDialogVisible ) {
self.saveSectionDialogVisible(false);
}
}
self.toggleSaveTmplUI(visible);
});
// Will store the collection of saved templates
self.allSavedTemplates = new api.Value('_not_populated_');
// When the collection is refreshed
// - populate select options
// - set the select value to default 'none'
self.allSavedTemplates.bind( function( tmpl_collection ) {
if ( !_.isObject(tmpl_collection) ) {
api.errare('error setupSaveTmplUI => tmpl collection should be an object');
return;
}
tmpl_collection = _.isEmpty(tmpl_collection) ? {} : tmpl_collection;
self.refreshTmplPickerHtml( tmpl_collection );
});
// Will store the collection of saved templates
self.allApiTemplates = new api.Value('_not_populated_');
self.tmplDialogMode = new api.Value('hidden');// 'save' default mode is set when dialog html is rendered
self.tmplDialogMode.bind( function(mode){
if ( !_.contains(['hidden', 'save', 'update', 'remove', 'edit' ], mode ) ) {
api.errare('::setupSaveTmplUI => unknown tmpl dialog mode', mode );
mode = 'save';
}
// Set the button pressed state
var $tmplDialogWrapper = $('#nimble-top-tmpl-save-ui'),
$titleInput = $tmplDialogWrapper.find('#sek-saved-tmpl-title'),
$descInput = $tmplDialogWrapper.find('#sek-saved-tmpl-description');
$tmplDialogWrapper.find('[data-tmpl-mode-switcher]').attr('aria-pressed', false );
$tmplDialogWrapper.find('[data-tmpl-mode-switcher="' + mode +'"]').attr('aria-pressed', true );
// update the current mode
$('#nimble-top-tmpl-save-ui').attr('data-sek-tmpl-dialog-mode', mode );
// make sure the remove dialog is hidden
$tmplDialogWrapper.removeClass('sek-removal-confirmation-opened');
var $selectEl;
// execute actions depending on the selected mode
switch( mode ) {
case 'save' :
// When selecting 'save', make sure the title and description input are cleaned
$titleInput.val('');
$descInput.val('');
break;
case 'update' :
case 'remove' :
$selectEl = $tmplDialogWrapper.find('.sek-saved-tmpl-picker');
// Make sure the select value is always reset when switching mode
$selectEl.val('none').trigger('change');
self.setSavedTmplCollection().done( function( tmpl_collection ) {
// refresh tmpl picker in case the user updated without changing anything
self.refreshTmplPickerHtml();
$selectEl.val( self.tmplToRemove || 'none' ).trigger('change');
self.tmplToRemove = null;
});
break;
case 'edit' :
$selectEl = $tmplDialogWrapper.find('.sek-saved-tmpl-picker');
// Make sure the select value is always reset when switching mode
$selectEl.val('none').trigger('change');
self.setSavedTmplCollection().done( function( tmpl_collection ) {
// refresh tmpl picker in case the user updated without changing anything
self.refreshTmplPickerHtml();
$selectEl.val( self.tmplToEdit || 'none' ).trigger('change');
self.tmplToEdit = null;
});
break;
}//switch
});//self.tmplDialogMode.bind()
},
///////////////////////////////////////////////
///// RENDER DIALOG BOX AND SCHEDULE CLICK ACTIONS
refreshTmplPickerHtml : function( tmpl_collection ) {
tmpl_collection = tmpl_collection || this.allSavedTemplates();
var $tmplDialogWrapper = $('#nimble-top-tmpl-save-ui'),
$selectEl = $tmplDialogWrapper.find('.sek-saved-tmpl-picker');
// Make sure the select value is always reset when switching mode
$selectEl.val('none').trigger('change');
// empty all options but the default 'none' one
$selectEl.find('option').each( function() {
if ( 'none' !== $(this).attr('value') ) {
$(this).remove();
}
});
// Make sure we don't populate the collection twice ( if user clicks two times fast )
// if ( $tmplDialogWrapper.hasClass('tmpl-collection-populated') )
// return;
var _default_title = 'template title not set',
_title,
_last_modified_date,
_html = '';
_.each( tmpl_collection, function( _tmpl_data, _tmpl_post_name ) {
if ( !_.isObject(_tmpl_data) )
return;
_last_modified_date = _tmpl_data.last_modified_date ? _tmpl_data.last_modified_date : '';
_title = _tmpl_data.title ? _tmpl_data.title : _default_title;
_html +='';
});
$selectEl.append(_html);
// flag so we know it's done
// => controls the CSS visibility of the select element
$tmplDialogWrapper.addClass('tmpl-collection-populated');
},
//@param = { }
renderTmplUI : function( params ) {
if ( $('#nimble-top-tmpl-save-ui').length > 0 )
return $('#nimble-top-tmpl-save-ui');
var self = this;
try {
_tmpl = wp.template( 'nimble-top-tmpl-save-ui' )( {} );
} catch( er ) {
api.errare( 'Error when parsing nimble-top-tmpl-save-ui template', er );
return false;
}
$('#customize-preview').after( $( _tmpl ) );
return $('#nimble-top-tmpl-save-ui');
},
///////////////////////////////////////////////
///// DOM EVENTS
// Fired once, on first rendering
scheduleTmplSaveDOMEvents : function() {
var self = this, $tmplDialogWrapper = $('#nimble-top-tmpl-save-ui');
if ( $tmplDialogWrapper.data('nimble-tmpl-dom-events-scheduled') )
return;
// ATTACH DOM EVENTS
// Dialog Mode Switcher
$tmplDialogWrapper.on( 'click', '[data-tmpl-mode-switcher]', function(evt) {
evt.preventDefault();
self.tmplDialogMode($(this).data('tmpl-mode-switcher'));
});
// React to template select
// update title and description fields on template selection
$tmplDialogWrapper.on( 'change', '.sek-saved-tmpl-picker', function(evt){ self.reactOnTemplateSelection(evt, $(this) ); });
// SAVE
$tmplDialogWrapper.on( 'click', '.sek-do-save-tmpl', function(evt){
$tmplDialogWrapper.addClass('nimble-tmpl-processing-ajax');
self.saveOrUpdateTemplate(evt).done( function( response ) {
$tmplDialogWrapper.removeClass('nimble-tmpl-processing-ajax');
if ( response.success ) {
self.tmplDialogVisible( false );
self.setSavedTmplCollection( { refresh : true } );// <= true for refresh
}
});
});
// UPDATE
$tmplDialogWrapper.on( 'click', '.sek-do-update-tmpl', function(evt){
var $selectEl = $tmplDialogWrapper.find('.sek-saved-tmpl-picker'),
tmplPostNameCandidateForUpdate = $selectEl.val();
// make sure we don't try to remove the default option
if ( 'none' === tmplPostNameCandidateForUpdate || _.isEmpty(tmplPostNameCandidateForUpdate) )
return;
$tmplDialogWrapper.addClass('nimble-tmpl-processing-ajax');
self.saveOrUpdateTemplate(evt, tmplPostNameCandidateForUpdate).done( function(response) {
$tmplDialogWrapper.removeClass('nimble-tmpl-processing-ajax');
if ( response.success ) {
self.tmplDialogVisible( false );
self.setSavedTmplCollection( { refresh : true } )// <= true for refresh
.done( function( tmpl_collection ) {
// refresh tmpl picker in case the user updated without changing anything
self.refreshTmplPickerHtml();
});
}
});
});
// REMOVE
// Reveal remove dialog
$tmplDialogWrapper.on( 'click', '.sek-open-remove-confirmation', function(evt){
$tmplDialogWrapper.addClass('sek-removal-confirmation-opened');
});
// Do Remove
$tmplDialogWrapper.on( 'click', '.sek-do-remove-tmpl', function(evt){
var $selectEl = $tmplDialogWrapper.find('.sek-saved-tmpl-picker'),
tmplPostNameCandidateForRemoval = $selectEl.val();
// make sure we don't try to remove the default option
if ( 'none' === tmplPostNameCandidateForRemoval || _.isEmpty(tmplPostNameCandidateForRemoval) )
return;
$tmplDialogWrapper.addClass('nimble-tmpl-processing-ajax');
self.removeTemplate(evt, tmplPostNameCandidateForRemoval).done( function(response) {
$tmplDialogWrapper.removeClass('nimble-tmpl-processing-ajax');
$tmplDialogWrapper.removeClass('sek-removal-confirmation-opened');
if ( response.success ) {
self.setSavedTmplCollection( { refresh : true } );// <= true for refresh
}
});
});
// Cancel Remove
$tmplDialogWrapper.on( 'click', '.sek-cancel-remove-tmpl', function(evt){
$tmplDialogWrapper.removeClass('sek-removal-confirmation-opened');
});
// Switch to update mode
//$tmplDialogWrapper.on( 'click', '[data-tmpl-mode-switcher="update"]', function(evt){ });
$('.sek-close-dialog', $tmplDialogWrapper ).on( 'click', function(evt) {
evt.preventDefault();
self.tmplDialogVisible(false);
});
// Say we're done with DOM event scheduling
$tmplDialogWrapper.data('nimble-tmpl-dom-events-scheduled', true );
},
// Is used in update and remove modes
reactOnTemplateSelection : function(evt, $selectEl ){
var self = this,
$tmplDialogWrapper = $('#nimble-top-tmpl-save-ui'),
_tmplPostName = $selectEl.val(),
$titleInput = $tmplDialogWrapper.find('#sek-saved-tmpl-title'),
$descInput = $tmplDialogWrapper.find('#sek-saved-tmpl-description'),
// The informative class control the visibility of the title and the description in CSS
_informativeClass = 'update' === self.tmplDialogMode() ? 'sek-tmpl-update-selected' : 'sek-tmpl-remove-selected';
if ( 'none' === _tmplPostName ) {
$titleInput.val('');
$descInput.val('');
$tmplDialogWrapper.removeClass(_informativeClass);
} else {
var _allSavedTemplates = self.allSavedTemplates();
var _selectedTmpl = _tmplPostName;
// normalize
_allSavedTemplates = ( _.isObject(_allSavedTemplates) && !_.isArray(_allSavedTemplates) ) ? _allSavedTemplates : {};
_allSavedTemplates[_tmplPostName] = $.extend( {
title : '',
description : '',
last_modified_date : ''
}, _allSavedTemplates[_tmplPostName] || {} );
$titleInput.val( _allSavedTemplates[_tmplPostName].title );
$descInput.val( _allSavedTemplates[_tmplPostName].description );
$tmplDialogWrapper.addClass(_informativeClass);
}
},
///////////////////////////////////////////////
///// AJAX ACTIONS
// Fired on 'click' on .sek-do-save-tmpl btn
saveOrUpdateTemplate : function(evt, tmplPostNameCandidateForUpdate ) {
var self = this, _dfd_ = $.Deferred();
evt.preventDefault();
var $_title = $('#sek-saved-tmpl-title'),
tmpl_title = $_title.val(),
tmpl_description = $('#sek-saved-tmpl-description').val(),
collectionSettingId = self.localSectionsSettingId(),
currentLocalSettingValue;
// Dec 2020 : remove locations not active on the page or empty before saving the tmpl
try { currentLocalSettingValue = self.preProcessTmpl( api( collectionSettingId )() ); } catch( er ) {
api.errorLog( 'error in ::saveOrUpdateTemplate', er );
_dfd_.resolve( {success:false});
}
if ( _.isEmpty( tmpl_title ) ) {
$_title.addClass('error');
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['A title is required'] + '',
''
].join('')
});
return _dfd_.resolve( {success:false});
}
$('#sek-saved-tmpl-title').removeClass('error');
wp.ajax.post( 'sek_save_user_template', {
nonce: api.settings.nonce.save,
tmpl_data: 'edit' === self.tmplDialogMode() ? '' : JSON.stringify( currentLocalSettingValue ),
// the following will be saved in 'metas'
tmpl_title: tmpl_title,
tmpl_description: tmpl_description,
tmpl_post_name: tmplPostNameCandidateForUpdate || '',// <= provided when updating a template
edit_metas_only: 'edit' === self.tmplDialogMode() ? 'yes' : 'no',//<= in this case we only update title and description. Not the template content
skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' ),
tmpl_locations : self.getActiveLocationsForTmpl( currentLocalSettingValue ),
tmpl_header_location : self.getHeaderOrFooterLocationIdForTmpl( 'header', currentLocalSettingValue ),
tmpl_footer_location : self.getHeaderOrFooterLocationIdForTmpl( 'footer', currentLocalSettingValue )
})
.done( function( response ) {
//console.log('SAVED POST ID', response );
_dfd_.resolve( {success:true});
// response is {tmpl_post_id: 436}
api.previewer.trigger('sek-notify', {
type : 'success',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Template saved'] + '',
''
].join('')
});
})
.fail( function( er ) {
_dfd_.resolve( {success:false});
api.errorLog( 'ajax sek_save_template => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Error when processing template'] + '',
''
].join('')
});
});
return _dfd_;
},//saveOrUpdateTemplate
// return an array of the location used for the template, to be used as template meta for a future injection
// we don't need to list all inactive locations of the current page in the template meta
getActiveLocationsForTmpl : function( tmpl_data ) {
if ( !_.isObject( tmpl_data ) ) {
throw new Error('preProcess Tmpl => error : tmpl_data must be an object');
}
var _tmpl_locations = [];
_.each( tmpl_data.collection, function( _loc_params ) {
if ( !_.isObject( _loc_params ) || !_loc_params.id || !_loc_params.level )
return;
if ( 'location' === _loc_params.level ) {
_tmpl_locations.push( _loc_params.id );
}
});
return _tmpl_locations;
},
getHeaderOrFooterLocationIdForTmpl : function( headerOrFooter, tmpl_data) {
var self = this;
if ( !_.isObject( tmpl_data ) ) {
throw new Error('preProcess Tmpl => error : tmpl_data must be an object');
}
var _loc = '';
_.each( tmpl_data.collection, function( _loc_params ) {
if ( !_.isObject( _loc_params ) || !_loc_params.id || !_loc_params.level )
return;
if ( ( 'header' === headerOrFooter && self.isHeaderLocation( _loc_params.id ) ) || ( 'footer' === headerOrFooter && self.isFooterLocation( _loc_params.id ) ) ) {
_loc = _loc_params.id;
}
});
return _loc;
},
// @return a tmpl model
// Note : ids are reset server side
// Example of tmpl model before preprocessing
// {
// collection: [{…}]
// id: "" //<= to remove
// level: "tmpl" // <= to remove
// options: {bg: {…}}
// ver_ini: "1.1.8"
// }
// Dec 2020 : remove locations not active on the page or empty before saving the tmpl
preProcessTmpl : function( tmpl_data ) {
var self = this, tmpl_processed, activeLocations;
if ( !_.isObject( tmpl_data ) ) {
throw new Error('preProcess Tmpl => error : tmpl_data must be an object');
}
tmpl_processed = $.extend( true, {}, tmpl_data );
tmpl_processed.collection = [];
activeLocations = self.activeLocations();
//console.log('TO DO => make sure template is ok to be saved', tmpl_data, self.activeLocations() );
_.each( tmpl_data.collection, function( _loc_params ) {
if ( !_.isObject( _loc_params ) || !_loc_params.id || !_loc_params.collection )
return;
if ( _.contains( activeLocations, _loc_params.id ) && !_.isEmpty(_loc_params.collection) ) {
tmpl_processed.collection.push( _loc_params );
}
});
return tmpl_processed;
},
// Fired on 'click on .sek-do-remove-tmpl btn
removeTemplate : function(evt, tmplPostNameCandidateForRemoval ) {
var self = this, _dfd_ = $.Deferred();
evt.preventDefault();
wp.ajax.post( 'sek_remove_user_template', {
nonce: api.settings.nonce.save,
tmpl_post_name: tmplPostNameCandidateForRemoval
//skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' )
})
.done( function( response ) {
_dfd_.resolve( {success:true});
// response is {tmpl_post_id: 436}
api.previewer.trigger('sek-notify', {
type : 'success',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Template removed'] + '',
''
].join('')
});
})
.fail( function( er ) {
_dfd_.resolve( {success:false});
api.errorLog( 'ajax sek_remove_template => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Error when processing templates'] + '',
''
].join('')
});
});
return _dfd_;
},
///////////////////////////////////////////////
///// REVEAL / HIDE DIALOG BOX
/// react on self.tmplDialogVisible.bind(...)
// @return void()
// self.tmplDialogVisible.bind( function( visible ){
// self.toggleSaveTmplUI( visible );
// });
toggleSaveTmplUI : function( visible ) {
visible = _.isUndefined( visible ) ? true : visible;
var self = this,
_renderAndSetup = function() {
$.when( self.renderTmplUI({}) ).done( function( $_el ) {
self.scheduleTmplSaveDOMEvents();
self.saveUIContainer = $_el;
//display
_.delay( function() {
// set dialog mode now so we display the relevant fields on init
self.tmplDialogMode('save');// Default mode is save
self.cachedElements.$body.addClass('sek-save-tmpl-ui-visible');
}, 200 );
// set tmpl id input value
//$('#sek-saved-tmpl-id').val( tmplId );
});
},
_hide = function() {
var dfd = $.Deferred();
self.cachedElements.$body.removeClass('sek-save-tmpl-ui-visible');
if ( $( '#nimble-top-tmpl-save-ui' ).length > 0 ) {
//remove Dom element after slide up
_.delay( function() {
// set dialog mode back to 'hidden' mode
self.tmplDialogMode = self.tmplDialogMode ? self.tmplDialogMode : new api.Value();
self.tmplDialogMode('hidden');
self.saveUIContainer.remove();
dfd.resolve();
}, 250 );
} else {
dfd.resolve();
}
return dfd.promise();
};
if ( visible ) {
_renderAndSetup();
} else {
_hide().done( function() {
self.tmplDialogVisible( false );//should be already false
});
}
},
///////////////////////////////////////////////
///// TMPL COLLECTION
// @return $.promise
setSavedTmplCollection : function( params ) {
var self = this, _dfd_ = $.Deferred();
// refresh is true on save, update, remove success
params = params || {refresh : false};
// If the collection is already set, return it.
// unless this is a "refresh" case
if ( !params.refresh && '_not_populated_' !== self.allSavedTemplates() ) {
return _dfd_.resolve( self.allSavedTemplates() );
}
var _promise;
// Prevent a double request while ajax request is being processed
if ( self.templateCollectionPromise && 'pending' === self.templateCollectionPromise.state() ) {
_promise = self.templateCollectionPromise;
} else {
_promise = self.getSavedTmplCollection();
}
_promise.done( function( tmpl_collection ) {
self.allSavedTemplates( tmpl_collection );
_dfd_.resolve( tmpl_collection );
});
return _dfd_.promise();
},
// @return a promise
getSavedTmplCollection : function() {
var self = this;
self.templateCollectionPromise = $.Deferred();
wp.ajax.post( 'sek_get_all_saved_tmpl', {
nonce: api.settings.nonce.save
//skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' )
})
.done( function( tmpl_collection ) {
if ( _.isObject(tmpl_collection) && !_.isArray( tmpl_collection ) ) {
self.templateCollectionPromise.resolve( tmpl_collection );
//console.log('GET SAVED TMPL COLLECTION', tmpl_collection );
} else {
self.templateCollectionPromise.resolve( {} );
if ( !_.isEmpty( tmpl_collection ) ) {
api.errare('control::getSavedTmplCollection => error => tmpl collection is invalid', tmpl_collection);
}
}
})
.fail( function( er ) {
api.errorLog( 'ajax sek_get_all_saved_tmpl => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Error when processing templates'] + '',
''
].join('')
});
self.templateCollectionPromise.resolve({});
});
return self.templateCollectionPromise;
},
// API TMPL COLLECTION
// @return a promise
getApiTmplCollection : function() {
var self = this,
dfd = $.Deferred(),
_collection = {};
if ( '_not_populated_' !== self.allApiTemplates() ) {
dfd.resolve( self.allApiTemplates() );
} else {
if ( !sektionsLocalizedData.useAPItemplates ) {
self.allApiTemplates([]);
dfd.resolve([]);
} else {
wp.ajax.post( 'sek_get_all_api_tmpl', {
nonce: api.settings.nonce.save
//skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' )
})
.done( function( tmpl_collection ) {
if ( _.isObject(tmpl_collection) && !_.isArray( tmpl_collection ) ) {
_collection = tmpl_collection;
//console.log('AJAX GET API TMPL COLLECTION DONE', tmpl_collection );
} else {
api.errare('control::getApiTmplCollection => error => tmpl collection is invalid', tmpl_collection);
}
self.allApiTemplates( _collection );
dfd.resolve( _collection );
})
.fail( function( er ) {
api.errorLog( 'ajax sek_get_all_api_tmpl => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'' + sektionsLocalizedData.i18n['Error when processing templates'] + '',
''
].join('')
});
dfd.resolve({});
});
}
}
return dfd;
},
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
// introduced in december 2020 for https://github.com/presscustomizr/nimble-builder/issues/655
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
////////////////////////////////////////////////////////
// INJECT TEMPLATE FROM GALLERY => FROM USER SAVED COLLECTION OR REMOTE API
////////////////////////////////////////////////////////
// @return promise
getTmplJsonFromUserTmpl : function( tmpl_name ) {
var self = this, _dfd_ = $.Deferred();
wp.ajax.post( 'sek_get_user_tmpl_json', {
nonce: api.settings.nonce.save,
tmpl_post_name: tmpl_name
//skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' )
})
.done( function( response ) {
_dfd_.resolve( {success:true, tmpl_json:response });
})
.fail( function( er ) {
_dfd_.resolve( {success:false});
api.errorLog( 'ajax getTmplJsonFromUserTmpl => error', er );
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 10000,
message : [
'',
'Error when fetching the template',
''
].join('')
});
});
return _dfd_;
},
getTmplJsonFromApi : function( tmpl_name, tmpl_is_pro ) {
var self = this, _dfd_ = $.Deferred();
wp.ajax.post( 'sek_get_api_tmpl_json', {
nonce: api.settings.nonce.save,
api_tmpl_name: tmpl_name,
api_tmpl_is_pro : tmpl_is_pro ? 'yes' : 'no'
//skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' )
})
.done( function( response ) {
_dfd_.resolve( {success:true, tmpl_json:response });
})
.fail( function( _r_ ) {
_dfd_.resolve( {success:false});
api.errorLog( 'ajax getTmplJsonFromApiTmpl => error', _r_ );
var _msg = 'Error when fetching the template';
if ( _.isString( _r_ ) && !_.isEmpty( _r_ ) ) {
_msg = _r_;
}
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 60000,
is_pro_notif : true,
notif_id : 'pro_tmpl_error',
message : [
'',
''+ _msg + '',
''
].join('')
});
});
return _dfd_;
},
// April 2020 : added for https://github.com/presscustomizr/nimble-builder/issues/651
// @param params {
// tmpl_name : string,
// tmpl_source : api_tmpl or user_tmpl,
// tmpl_inject_mode : 3 possible import modes : replace, before, after,
// tmpl_is_pro: false
// }
get_gallery_tmpl_json_and_inject : function( params ) {
var self = this;
params = $.extend( {
tmpl_name : '',
tmpl_source : 'user',
tmpl_inject_mode : 'replace',
tmpl_is_pro: false
}, params || {});
var tmpl_name = params.tmpl_name;
if ( _.isEmpty( tmpl_name ) || ! _.isString( tmpl_name ) ) {
api.errare('::tmpl inject => error => invalid template name');
}
var _promise;
if ( 'api_tmpl' === params.tmpl_source ) {
// doc : https://api.jquery.com/jQuery.getJSON/
_promise = self.getTmplJsonFromApi(tmpl_name, params.tmpl_is_pro);
} else {
_promise = self.getTmplJsonFromUserTmpl(tmpl_name);
}
// response object structure :
// {
// data : { nimble content },
// metas : {
// skope_id :
// version :
// tmpl_locations :
// date :
// theme :
// }
// }
_promise.done( function( response ) {
if ( response.success ) {
// api.infoLog('INJECT NIMBLE TEMPLATE', params );
self.inject_tmpl_from_gallery({
tmpl_name : tmpl_name,
template_data : response.tmpl_json,
tmpl_inject_mode : params.tmpl_inject_mode,
tmpl_is_pro: params.tmpl_is_pro
});
} else {
// Clean loader if failed response
api.previewer.send( 'sek-clean-loader', { cleanFullPageLoader : true });
}
}).fail( function() {
api.previewer.send( 'sek-clean-loader', { cleanFullPageLoader : true });
});
// After 30 s display a failure notification
// april 2020 : introduced for https://github.com/presscustomizr/nimble-builder/issues/663
_.delay( function() {
if ( 'pending' !== _promise.state() )
return;
api.previewer.trigger('sek-notify', {
notif_id : 'import-too-long',
type : 'error',
duration : 20000,
message : [
'',
'',
'Template import failed',
'',
''
].join('')
});
}, 30000 );
return _promise;
},
// INJECT TEMPLATE FROM GALLERY
// => REMOTE API COLLECTION + USER COLLECTION
// @param params
// {
// tmpl_name : tmpl_name,
// template_data : response.tmpl_json,
// tmpl_inject_mode : 3 possible import modes : replace, before, after
// tmpl_is_pro : false
// }
inject_tmpl_from_gallery : function( params ) {
var self = this;
params = params || {};
// normalize params
params = $.extend({
tmpl_inject_mode : 'replace'
}, params );
var _scope = 'local';//<= when injecting a template not manually, scope is always local
// remote template inject case
if ( !params.template_data ) {
throw new Error( '::inject_tmpl => missing remote template data' );
}
/////////////////////////////////////////////
/// 1) CHECK IF CONTENT IS WELL FORMED AND ELIGIBLE FOR API
/// 2) LET'S PROCESS THE SETTING ID'S
/// 3) ATTEMPT TO UPDATE THE SETTING API, LOCAL OR GLOBAL. ( always local for template inject )
if ( !api.czr_sektions.isImportedContentEligibleForAPI( {success:true, data:params.template_data}, params ) ) {
api.infoLog('::inject_tmpl problem => !api.czr_sektions.isImportedContentEligibleForAPI', params );
return;
}
params.template_data.data.collection = self.setIdsForImportedTmpl( params.template_data.data.collection );
// and try to update the api setting
api.czr_sektions.doUpdateApiSettingAfter_TmplGalleryImport( {success:true, data:params.template_data}, params );
},//inject_tmpl_from_gallery
// fired on ajaxrequest done
// At this stage, the server_resp data structure has been validated.
// We can try to the update the api setting
// @param params
// {
// tmpl_name : tmpl_name,
// template_data : response.tmpl_json,
// tmpl_inject_mode : 3 possible import modes : replace, before, after,
// tmpl_is_pro : false
// }
doUpdateApiSettingAfter_TmplGalleryImport : function( server_resp, params ){
params = params || {};
if ( !api.czr_sektions.isImportedContentEligibleForAPI( server_resp, params ) ) {
api.previewer.send( 'sek-clean-loader', { cleanFullPageLoader : true });
return;
}
var _scope = 'local';// <= always local when template gallery inject
//api.infoLog('TODO => verify metas => version, active locations, etc ... ');
// Update the setting api via the normalized method
// the scope will determine the setting id, local or global
api.czr_sektions.updateAPISetting({
action : 'sek-inject-tmpl-from-gallery',
scope : _scope,//'global' or 'local'<= will determine which setting will be updated,
// => self.getGlobalSectionsSettingId() or self.localSectionsSettingId()
injected_content : server_resp.data,
tmpl_inject_mode : params.tmpl_inject_mode
}).done( function() {
// Clean an regenerate the local option setting
// Settings are normally registered once and never cleaned, unlike controls.
// After the inject, updating the setting value will refresh the sections
// but the local options, persisted in separate settings, won't be updated if the settings are not cleaned
if ( 'local' === _scope ) {
var _doThingsAfterRefresh = function() {
// Removes and RE-register local settings and controls
api.czr_sektions.generateUI({
action : 'sek-generate-local-skope-options-ui',
clean_settings_and_controls_first : true//<= see api.czr_sektions.generateUIforLocalSkopeOptions()
});
api.previewer.unbind( 'czr-new-skopes-synced', _doThingsAfterRefresh );
};
// 'czr-new-skopes-synced' is always sent on a previewer.refresh()
api.previewer.bind( 'czr-new-skopes-synced', _doThingsAfterRefresh );
}
//_notify( sektionsLocalizedData.i18n['The revision has been successfully restored.'], 'success' );
api.previewer.refresh();
api.previewer.trigger('sek-notify', {
notif_id : 'import-success',
type : 'success',
duration : 30000,
message : [
'',
'',
sektionsLocalizedData.i18n['Template successfully imported'],
'',
''
].join('')
});
}).fail( function( response ) {
api.errare( '::doUpdateApiSettingAfter_TmplGalleryImport => error when firing ::updateAPISetting', response );
api.previewer.trigger('sek-notify', {
notif_id : 'import-failed',
type : 'error',
duration : 30000,
message : [
'',
'',
[ sektionsLocalizedData.i18n['Import failed'], response ].join(' : '),
'',
''
].join('')
});
});
// Refresh the preview, so the markup is refreshed and the css stylesheet are generated
api.previewer.refresh();
}//doUpdateApiSettingAfter_TmplGalleryImport()
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// the input id determine if we fetch the revision history of the local or global setting
// @return a deferred promise
// @params object : { is_local:bool} <= 'local_revisions' === input.id
getRevisionHistory : function(params) {
return wp.ajax.post( 'sek_get_revision_history', {
nonce: api.settings.nonce.save,
skope_id : params.is_local ? api.czr_skopeBase.getSkopeProperty( 'skope_id' ) : sektionsLocalizedData.globalSkopeId
});
},
// @return void()
// Fetches the_content and try to set the setting value through normalized ::updateAPISetting method
// @params {
// is_local : bool//<= 'local_revisions' === input.id
// revision_post_id : int
// }
setSingleRevision : function(params) {
var self = this;
var _notify = function( message, type ) {
api.previewer.trigger('sek-notify', {
notif_id : 'restore-revision-error',
type : type || 'info',
duration : 10000,
message : [
'',
'',
message || '',
'',
''
].join('')
});
};
wp.ajax.post( 'sek_get_single_revision', {
nonce: api.settings.nonce.save,
//skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),
revision_post_id : params.revision_post_id
}).done( function( revision_value ){
// If the setting value is unchanged, no need to go further
// is_local is decided with the input id => @see revision_history input type.
var setId = params.is_local ? self.localSectionsSettingId() : self.getGlobalSectionsSettingId();
if ( _.isEqual( api( setId )(), revision_value ) ) {
_notify( sektionsLocalizedData.i18n['This is the current version.'], 'info' );
return;
}
// api.infoLog( 'getSingleRevision response', revision_value );
// api.infoLog( 'Current val', api(self.localSectionsSettingId())() );
self.updateAPISetting({
action : 'sek-restore-revision',
is_global_location : !params.is_local,//<= will determine which setting will be updated,
// => self.getGlobalSectionsSettingId() or self.localSectionsSettingId()
revision_value : revision_value
}).done( function() {
//_notify( sektionsLocalizedData.i18n['The revision has been successfully restored.'], 'success' );
api.previewer.refresh();
}).fail( function( response ) {
api.errare( '::setSingleRevision error when firing ::updateAPISetting', response );
_notify( sektionsLocalizedData.i18n['The revision could not be restored.'], 'error' );
});
//api.previewer.refresh();
}).fail( function( response ) {
api.errare( '::setSingleRevision ajax error', response );
_notify( sektionsLocalizedData.i18n['The revision could not be restored.'], 'error' );
});
}
});//$.extend()
})( wp.customize, jQuery );
//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// Fired on api 'ready', in reaction to ::setContextualCollectionSettingIdWhenSkopeSet => ::localSectionsSettingId
// 1) register the collection setting nimble___[{$skope_id}] ( ex : nimble___[skp__post_page_20] )
// 2) validate that the setting is well formed before being changed
// 3) schedule reactions on change ?
// @return void()
setupSettingsToBeSaved : function( params ) {
var self = this,
serverCollection;
params = params || { dirty : false, is_group_inheritance_enabled : true };
// maybe register the sektion_collection settings
var _settingsToRegister_ = {
'local' : { collectionSettingId : self.localSectionsSettingId() },//<= "nimble___[skp__post_page_10]"
'global' : { collectionSettingId : self.getGlobalSectionsSettingId() }//<= "nimble___[skp__global]"
};
_.each( _settingsToRegister_, function( settingData, localOrGlobal ) {
serverCollection = api.czr_skopeBase.getSkopeProperty( 'sektions', localOrGlobal ).db_values;
if ( _.isEmpty( settingData.collectionSettingId ) ) {
throw new Error( 'setupSettingsToBeSaved => the collectionSettingId is invalid' );
}
// if the collection setting is not registered yet
// => register it and bind it
// => ensure that it will be bound only once, because the setting are never unregistered
if ( ! api.has( settingData.collectionSettingId ) ) {
var inital_setting_value = _.isObject( serverCollection ) ? serverCollection : self.getDefaultSektionSettingValue( localOrGlobal );
if ( 'local' == localOrGlobal && !params.is_group_inheritance_enabled ) {
inital_setting_value.local_options.local_reset = _.isObject( inital_setting_value.local_options.local_reset ) ? inital_setting_value.local_options.local_reset : {};
inital_setting_value.local_options.local_reset.inherit_group_scope = false;
}
var __collectionSettingInstance__ = api.CZR_Helpers.register({
what : 'setting',
id : settingData.collectionSettingId,
value : self.validateSettingValue( inital_setting_value, localOrGlobal ),
transport : 'postMessage',//'refresh'
type : 'option',
track : false,//don't register in the self.registered()
origin : 'nimble',
dirty : params.dirty
});
//if ( sektionsLocalizedData.isDevMode ) {}
api( settingData.collectionSettingId, function( sektionSetInstance ) {
// Schedule reactions to a collection change
sektionSetInstance.bind( _.debounce( function( newSektionSettingValue, previousValue, params ) {
// api.infoLog( 'sektionSettingValue is updated',
// {
// newValue : newSektionSettingValue,
// previousValue : previousValue,
// params : params
// }
// );
// console.log('MAIN SETTING CHANGED', params );
//console.log('NEW MAIN SETTING VALUE', newSektionSettingValue );
// Track changes, if not already navigating the logs
if ( !_.isObject( params ) || true !== params.navigatingHistoryLogs ) {
try { self.trackHistoryLog( sektionSetInstance, params ); } catch(er) {
api.errare( 'setupSettingsToBeSaved => trackHistoryLog', er );
}
}
// April 2021 : for site templates
if ( 'local' === localOrGlobal ) {
api.trigger('nimble-update-topbar-skope-status');
}
}, 1000 ) );
});//api( settingData.collectionSettingId, function( sektionSetInstance ){}
}//if ( ! api.has( settingData.collectionSettingId ) ) {
});//_.each(
// global options for all collection setting of this skope_id
// loop_start, before_content, after_content, loop_end
// Global Options : section
// api.CZR_Helpers.register({
// what : 'section',
// id : sektionsLocalizedData.optPrefixForSektionGlobalOptsSetting,//'__sektions__'
// title: 'Global Options',
// priority : 1000,
// constructWith : SektionPanelConstructor,
// track : false//don't register in the self.registered()
// });
// // => register a control
// // Template
// api.CZR_Helpers.register({
// what : 'control',
// id : sektionsLocalizedData.sektionsPanelId,//'__sektions__'
// title: 'Main sektions panel',
// priority : 1000,
// constructWith : SektionPanelConstructor,
// track : false//don't register in the self.registered()
// });
},// SetupSettingsToBeSaved()
// Fired :
// 1) when instantiating the setting
// 2) on each setting change, as an override of api.Value::validate( to ) @see customize-base.js
// 3) directly when navigating the history log
// 4) when importing locally or globally
// @return {} or null if did not pass the checks
// @param scope = string, local or global
validateSettingValue : function( valCandidate, scope ) {
if ( ! _.isObject( valCandidate ) ) {
api.errare('::validateSettingValue => validation error => the setting should be an object', valCandidate );
return null;
}
if ( _.isEmpty( scope ) || !_.contains(['local', 'global'], scope ) ) {
api.errare( '::validateSettingValue => invalid scope provided.', scope );
return;
}
var parentLevel = {},
errorDetected = false,
levelIds = [],
authorized_local_option_groups = ['collection', 'local_options', 'fonts', '__inherits_group_skope_tmpl_when_exists__' ];
// walk the collections tree and verify it passes the various consistency checks
var _errorDetected_ = function( msg ) {
api.errare( msg , valCandidate );
if ( sektionsLocalizedData.isDevMode ) {
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 60000,
message : [
'',
'' + msg + '',
' ',
sektionsLocalizedData.i18n['If this problem locks Nimble Builder, you can try resetting the sections of this page.'],
' ',
'',
'',
'',
''
].join('')
});
}
errorDetected = true;
};
var _checkWalker_ = function( level ) {
if ( errorDetected ) {
return;
}
if ( _.isUndefined( level ) && _.isEmpty( parentLevel ) ) {
// we are at the root level
level = $.extend( true, {}, valCandidate );
if ( _.isUndefined( level.id ) || _.isUndefined( level.level ) ) {
// - there should be no 'level' property or 'id'
// - there should be a collection of registered locations
// - there should be no parent level defined
if ( _.isUndefined( level.collection ) ) {
_errorDetected_( 'validation error => the root level is missing the collection of locations' );
return;
}
if ( ! _.isEmpty( level.level ) || ! _.isEmpty( level.id ) ) {
_errorDetected_( 'validation error => the root level should not have a "level" or an "id" property' );
return;
}
// the local setting is structured this way:
// {
// collection : [],
// local_options : {},
// fonts : []
// }
//
// global_options like sitewide header and footer are saved in a specific option => NIMBLE_OPT_NAME_FOR_GLOBAL_OPTIONS
// the global setting is structured this way:
// {
// collection : [],
// fonts : []
// }
// Make sure that there's no unauthorized option group at root level
_.each( level, function( _opts, _opt_group_name) {
switch( scope ) {
case 'local' :
if( !_.contains( authorized_local_option_groups , _opt_group_name ) ) {
_errorDetected_( 'validation error => unauthorized option group for local setting value => ' + _opt_group_name );
return;
}
break;
case 'global' :
if( !_.contains( ['collection', 'fonts' ] , _opt_group_name ) ) {
_errorDetected_( 'validation error => unauthorized option group for global setting value => ' + _opt_group_name );
return;
}
break;
}
});
// Walk the section collection
_.each( valCandidate.collection, function( _l_ ) {
// Set the parent level now
parentLevel = level;
// walk
_checkWalker_( _l_ );
});
}
} else {
// we have a level.
// - make sure we have at least the following properties : id, level
// LEVEL should be an object
if ( _.isUndefined( level ) || !_.isObject( level ) ) {
_errorDetected_('validation error => a level is invalid' );
return;
}
// ID
if ( _.isEmpty( level.id ) || ! _.isString( level.id )) {
_errorDetected_('validation error => a ' + level.level + ' level must have a valid id' );
return;
} else if ( _.contains( levelIds, level.id ) ) {
_errorDetected_('validation error => duplicated level id : ' + level.id );
return;
} else {
levelIds.push( level.id );
}
// OPTIONS
// if ( _.isEmpty( level.options ) || ! _.isObject( level.options )) {
// _errorDetected_('validation error => a ' + level.level + ' level must have a valid options property' );
// return;
// }
// LEVEL
if ( _.isEmpty( level.level ) || ! _.isString( level.level ) ) {
_errorDetected_('validation error => a ' + level.level + ' level must have a level property' );
return;
} else if ( ! _.contains( [ 'location', 'section', 'column', 'module' ], level.level ) ) {
_errorDetected_('validation error => the level "' + level.level + '" is not authorized' );
return;
}
// - Unless we are in a module, there should be a collection property
// - make sure a module doesn't have a collection property
if ( 'module' == level.level ) {
if ( ! _.isUndefined( level.collection ) ) {
_errorDetected_('validation error => a module can not have a collection property' );
return;
}
} else {
if ( _.isUndefined( level.collection ) ) {
_errorDetected_( 'validation error => missing collection property for level => ' + level.level + ' ' + level.id );
return;
}
}
// a level should always have a version "ver_ini" property
if ( _.isUndefined( level.ver_ini ) ) {
//_errorDetected_('validation error => a ' + level.level + ' should have a version property : "ver_ini"' );
//return;
api.errare( 'validateSettingValue() => validation error => a ' + level.level + ' should have a version property : "ver_ini"' );
}
// Specific checks by level type
switch ( level.level ) {
case 'location' :
if ( ! _.isEmpty( parentLevel.level ) ) {
_errorDetected_('validation error => the parent of location ' + level.id +' should have no level set' );
return;
}
break;
case 'section' :
if ( level.is_nested && 'column' != parentLevel.level ) {
_errorDetected_('validation error => the nested section ' + level.id +' must be child of a column' );
return;
}
if ( ! level.is_nested && 'location' != parentLevel.level ) {
_errorDetected_('validation error => the section ' + level.id +' must be child of a location' );
return;
}
break;
case 'column' :
if ( 'section' != parentLevel.level ) {
_errorDetected_('validation error => the column ' + level.id +' must be child of a section' );
return;
}
break;
case 'module' :
if ( 'column' != parentLevel.level ) {
_errorDetected_('validation error => the module ' + level.id +' must be child of a column' );
return;
}
break;
}
// If we are not in a module, keep walking the collections
if ( 'module' != level.level ) {
_.each( level.collection, function( _l_ ) {
// Set the parent level now
parentLevel = $.extend( true, {}, level );
if ( ! _.isUndefined( _l_ ) ) {
// And walk sub levels
_checkWalker_( _l_ );
} else {
_errorDetected_('validation error => undefined level ' );
}
});
}
}
};
_checkWalker_();
if ( errorDetected ) {
api.infoLog('error in ::validateSettingValue', valCandidate );
return null;
}
//api.infoLog('in ::validateSettingValue', valCandidate );
// if null is returned, the setting value is not set @see customize-base.js
return valCandidate;
},//validateSettingValue
// triggered when clicking on [data-sek-reset="true"]
// click event is scheduled in ::initialize()
// Note : only the collection is set to self.getDefaultSektionSettingValue( 'local' )
// @see php function which defines the defaults sek_get_default_location_model()
resetCollectionSetting : function( scope, localOptions ) {
var self = this, newSettingValue;
if ( _.isEmpty( scope ) || !_.contains(['local', 'global'], scope ) ) {
throw new Error( 'resetCollectionSetting => invalid scope provided.', scope );
}
// INHERITANCE
// solves the problem of preventing group template inheritance after a local reset
var _is_inheritance_enabled_in_local_options = true;
if ( 'local' === scope ) {
if ( localOptions && _.isObject(localOptions) && localOptions.local_reset && !_.isUndefined( localOptions.local_reset.inherit_group_scope ) ) {
_is_inheritance_enabled_in_local_options = localOptions.local_reset.inherit_group_scope;
}
}
newSettingValue = $.extend( true, {}, self.getDefaultSektionSettingValue( scope ) );
if ( !_is_inheritance_enabled_in_local_options ) {
newSettingValue.local_options.local_reset = { inherit_group_scope : false };
}
// April 2021, for site templates #478 => the default local sektion model includes property __inherits_group_skope_tmpl_when_exists__, set to true
// => when reseting locally, if a group template is defined, it will be inherited
// How does it work?
// When a page has not been locally customized, property __inherits_group_skope_tmpl_when_exists__ is true ( @see sek_get_default_location_model() )
// As soon as the main local setting id is modified, __inherits_group_skope_tmpl_when_exists__ is set to false ( see js control::updateAPISetting )
// After a reset case, NB sets __inherits_group_skope_tmpl_when_exists__ back to true here
// Note : If this property is set to true => NB removes the local skope post in Nimble_Collection_Setting::update()
return newSettingValue;
}
});//$.extend()
})( wp.customize, jQuery );//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// invoked on api('ready') from self::initialize()
// update the main setting OR generate a UI in the panel
// AND
// always send back a confirmation to the preview, so we can fire the ajax actions
// the message sent back is used in particular to
// - always pass the location_skope_id, which otherwise would be impossible to get in ajax
// - in a duplication case, to pass the the newly generated id of the cloned level
reactToPreviewMsg : function() {
var self = this,
apiParams = {},
uiParams = {},
sendToPreview = true, //<= the default behaviour is to send a message to the preview when the setting has been changed
msgCollection = {
// A section can be added in various scenarios :
// - when clicking on the ( + ) Insert content => @see preview::scheduleUiClickReactions() => addContentButton
// - when adding a nested section to a column
// - when dragging a module in a 'between-sections' or 'in-empty-location' drop zone
//
// Note : if the target location level already has section(s), then the section is appended in ajax, at the right place
// Note : if the target location is empty ( is_first_section is true ), nothing is send to the preview when updating the api setting, and we refresh the location level. => this makes sure that we removes the placeholder printed in the previously empty location
'sek-add-section' : {
callback : function( params ) {
// July 2020 => for #728
api.previewedDevice( 'desktop' );
sendToPreview = ! _.isUndefined( params.send_to_preview ) ? params.send_to_preview : true;//<= when the level is refreshed when complete, we don't need to send to preview.
uiParams = {};
apiParams = {
action : 'sek-add-section',
id : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(),
location : params.location,
in_sektion : params.in_sektion,
in_column : params.in_column,
is_nested : ! _.isEmpty( params.in_sektion ) && ! _.isEmpty( params.in_column ),
before_section : params.before_section,
after_section : params.after_section,
is_first_section : params.is_first_section
};
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
// When a section is created ( not duplicated )
if ( params.apiParams.is_first_section ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.location
});
}
api.previewer.trigger( 'sek-pick-content', {
// the "id" param is added to set the target for double click insertion
// implemented for https://github.com/presscustomizr/nimble-builder/issues/317
id : params.apiParams ? params.apiParams.id : '',
content_type : 'section'
});
api.previewer.send('sek-animate-to-level', { id : params.apiParams.id });
}
},
'sek-add-column' : {
callback : function( params ) {
sendToPreview = true;
uiParams = {};
apiParams = {
id : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(),
action : 'sek-add-column',
in_sektion : params.in_sektion,
autofocus : params.autofocus
};
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
// When adding a section, a nested column is automatically added
// We want to focus on the module picker in this case, that's why the autofocus is set to false
// @see 'sek-add-section' action description
if ( false !== params.apiParams.autofocus ) {
api.previewer.trigger( 'sek-pick-content', {});
}
}
},
'sek-add-module' : {
callback :function( params ) {
sendToPreview = true;
uiParams = {};
apiParams = {
id : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(),
action : 'sek-add-module',
in_sektion : params.in_sektion,
in_column : params.in_column,
module_type : params.content_id,
before_module_or_nested_section : params.before_module_or_nested_section,
after_module_or_nested_section : params.after_module_or_nested_section
};
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
api.previewer.trigger( 'sek-edit-module', {
id : params.apiParams.id,
level : 'module',
in_sektion : params.apiParams.in_sektion,
in_column : params.apiParams.in_column
});
// always update the root fonts property after a module addition
// because there might be a google font specified in the starting value
self.updateAPISetting({
action : 'sek-update-fonts',
is_global_location : self.isGlobalLocation( params.apiParams )
});
// Refresh the stylesheet to generate the css rules of the clone
// api.previewer.send( 'sek-refresh-stylesheet', {
// location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
// });
api.previewer.trigger('sek-refresh-stylesheet', {
id : params.apiParams.in_column,
location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' )//<= send skope id to the preview so we can use it when ajaxing
});
}
},
'sek-remove' : {
callback : function( params ) {
sendToPreview = true;
uiParams = {};
switch( params.level ) {
case 'section' :
var sektionToRemove = self.getLevelModel( params.id );
if ( 'no_match' === sektionToRemove ) {
api.errare( 'reactToPreviewMsg => sek-remove-section => no sektionToRemove matched' );
break;
}
apiParams = {
action : 'sek-remove-section',
id : params.id,
location : params.location,
in_sektion : params.in_sektion,
in_column : params.in_column,
is_nested : sektionToRemove.is_nested
};
break;
case 'column' :
apiParams = {
action : 'sek-remove-column',
id : params.id,
in_sektion : params.in_sektion
};
break;
case 'module' :
apiParams = {
action : 'sek-remove-module',
id : params.id,
in_sektion : params.in_sektion,
in_column : params.in_column
};
break;
default :
api.errare( '::reactToPreviewMsg => sek-remove => missing level ', params );
break;
}
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
api.previewer.trigger( 'sek-pick-content', {});
// always update the root fonts property after a removal
// because the removed level(s) might had registered fonts
self.updateAPISetting({
action : 'sek-update-fonts',
is_global_location : self.isGlobalLocation( params.apiParams )
});
// When the last section of a location gets removed, make sure we refresh the location level, to print the sek-empty-location-placeholder
if ( 'sek-remove-section' === params.apiParams.action ) {
var locationLevel = self.getLevelModel( params.apiParams.location );
if ( _.isEmpty( locationLevel.collection ) ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.location
});
}
}
}
},
'sek-move' : {
callback : function( params ) {
sendToPreview = true;
uiParams = {};
switch( params.level ) {
case 'section' :
apiParams = {
action : 'sek-move-section',
id : params.id,
is_nested : ! _.isEmpty( params.in_sektion ) && ! _.isEmpty( params.in_column ),
newOrder : params.newOrder,
from_location : params.from_location,
to_location : params.to_location
};
break;
case 'column' :
apiParams = {
action : 'sek-move-column',
id : params.id,
newOrder : params.newOrder,
from_sektion : params.from_sektion,
to_sektion : params.to_sektion,
};
break;
case 'module' :
apiParams = {
action : 'sek-move-module',
id : params.id,
newOrder : params.newOrder,
from_column : params.from_column,
to_column : params.to_column,
from_sektion : params.from_sektion,
to_sektion : params.to_sektion,
};
break;
}
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
switch( params.apiParams.action ) {
case 'sek-move-section' :
api.previewer.trigger('sek-edit-options', {
id : params.apiParams.id,
level : 'section',
in_sektion : params.apiParams.id
});
// refresh location levels if the source and target location are differents
if ( params.apiParams.from_location != params.apiParams.to_location ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.to_location
});
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.from_location
});
}
break;
case 'sek-move-column' :
api.previewer.trigger('sek-edit-options', {
id : params.apiParams.id,
level : 'column',
in_sektion : params.apiParams.in_sektion,
in_column : params.apiParams.in_column
});
break;
case 'sek-refresh-modules-in-column' :
api.previewer.trigger('sek-edit-module', {
id : params.apiParams.id,
level : 'module',
in_sektion : params.apiParams.in_sektion,
in_column : params.apiParams.in_column
});
break;
}
}
},//sek-move
'sek-move-section-up' : {
callback : function( params ) {
sendToPreview = false;
uiParams = {};
apiParams = {
action : 'sek-move-section-up-down',
direction : 'up',
id : params.id,
is_nested : ! _.isEmpty( params.in_sektion ) && ! _.isEmpty( params.in_column ),
location : params.location,
in_column : params.in_column//<= will be used when moving a nested section
};
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.location,
// added for https://github.com/presscustomizr/nimble-builder/issues/471
original_action : 'sek-move-section-up',
moved_level_id : params.apiParams.id
});
// Introduced for https://github.com/presscustomizr/nimble-builder/issues/521
if ( params.apiParams.new_location ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.new_location,
// added for https://github.com/presscustomizr/nimble-builder/issues/471
original_action : 'sek-move-section-down',
moved_level_id : params.apiParams.id
});
}
}
},
'sek-move-section-down' : {
callback : function( params ) {
sendToPreview = false;
uiParams = {};
apiParams = {
action : 'sek-move-section-up-down',
direction : 'down',
id : params.id,
is_nested : ! _.isEmpty( params.in_sektion ) && ! _.isEmpty( params.in_column ),
location : params.location,
in_column : params.in_column//<= will be used when moving a nested section
};
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.location,
// added for https://github.com/presscustomizr/nimble-builder/issues/471
original_action : 'sek-move-section-down',
moved_level_id : params.apiParams.id
});
// Introduced for https://github.com/presscustomizr/nimble-builder/issues/521
if ( params.apiParams.new_location ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.new_location,
// added for https://github.com/presscustomizr/nimble-builder/issues/471
original_action : 'sek-move-section-down',
moved_level_id : params.apiParams.id
});
}
}
},
// the level will be cloned and walked to replace all ids by new one
// then the level clone id will be send back to the preview for the ajax rendering ( this is done in updateAPISetting() promise() )
'sek-duplicate' : {
callback : function( params ) {
sendToPreview = true;
uiParams = {};
switch( params.level ) {
case 'section' :
apiParams = {
action : 'sek-duplicate-section',
id : params.id,
location : params.location,
in_sektion : params.in_sektion,
in_column : params.in_column,
is_nested : ! _.isEmpty( params.in_sektion ) && ! _.isEmpty( params.in_column )
};
break;
case 'column' :
apiParams = {
action : 'sek-duplicate-column',
id : params.id,
in_sektion : params.in_sektion,
in_column : params.in_column
};
break;
case 'module' :
apiParams = {
action : 'sek-duplicate-module',
id : params.id,
in_sektion : params.in_sektion,
in_column : params.in_column
};
break;
}
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
var idForStyleSheetRefresh;
switch( params.apiParams.action ) {
case 'sek-duplicate-section' :
api.previewer.trigger('sek-edit-options', {
id : params.apiParams.id,
level : 'section',
in_sektion : params.apiParams.id
});
idForStyleSheetRefresh = params.apiParams.location;
//introduced for https://github.com/presscustomizr/nimble-builder/issues/617
if ( params.apiParams.is_nested ) {
api.previewer.refresh();
}
// Focus on the cloned level
api.previewer.send('sek-animate-to-level', { id : params.apiParams.id });
break;
case 'sek-duplicate-column' :
api.previewer.trigger('sek-edit-options', {
id : params.apiParams.id,
level : 'column',
in_sektion : params.apiParams.in_sektion,
in_column : params.apiParams.in_column
});
idForStyleSheetRefresh = params.apiParams.in_sektion;
break;
case 'sek-duplicate-module' :
api.previewer.trigger('sek-edit-module', {
id : params.apiParams.id,
level : 'module',
in_sektion : params.apiParams.in_sektion,
in_column : params.apiParams.in_column
});
idForStyleSheetRefresh = params.apiParams.in_column;
break;
}
// Refresh the stylesheet to generate the css rules of the clone
// api.previewer.send( 'sek-refresh-stylesheet', {
// location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
// });
api.previewer.trigger('sek-refresh-stylesheet', {
id : idForStyleSheetRefresh,
location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' )//<= send skope id to the preview so we can use it when ajaxing
});
}
},
'sek-resize-columns' : function( params ) {
sendToPreview = true;
uiParams = {};
apiParams = params;
return self.updateAPISetting( apiParams );
},
// @params {
// drop_target_element : $(this),
// position : _position,
// before_section : $(this).data('sek-before-section'),
// after_section : $(this).data('sek-after-section'),
// content_type : event.originalEvent.dataTransfer.getData( "sek-content-type" ),
// content_id : event.originalEvent.dataTransfer.getData( "sek-content-id" )
// }
'sek-add-content-in-new-sektion' : {
callback : function( params ) {
sendToPreview = ! _.isUndefined( params.send_to_preview ) ? params.send_to_preview : true;//<= when the level is refreshed when complete, we don't need to send to preview.
uiParams = {};
apiParams = params;
apiParams.action = 'sek-add-content-in-new-sektion';
apiParams.id = sektionsLocalizedData.prefixForSettingsNotSaved + self.guid();//we set the id here because it will be needed when ajaxing
switch( params.content_type) {
// When a module is dropped in a section + column structure to be generated
case 'module' :
apiParams.droppedModuleId = sektionsLocalizedData.prefixForSettingsNotSaved + self.guid();//we set the id here because it will be needed when ajaxing
break;
// When a preset section is dropped
case 'preset_section' :
api.previewer.send( 'sek-maybe-print-loader', { loader_located_in_level_id : params.location });
api.previewer.send( 'sek-maybe-print-loader', { fullPageLoader : true });
break;
}
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
switch( params.apiParams.content_type) {
case 'module' :
api.previewer.trigger('sek-edit-module', {
level : 'module',
id : params.apiParams.droppedModuleId
});
break;
// Clean the full page loader if not autocleaned yet
case 'preset_section' :
api.previewer.send( 'sek-clean-loader', { cleanFullPageLoader : true });
break;
}
// Always update the root fonts property after a module addition
// => because there might be a google font specified in the starting value or in a preset section
self.updateAPISetting({
action : 'sek-update-fonts',
is_global_location : self.isGlobalLocation( params.apiParams )
});
// Refresh the stylesheet to generate the css rules of the clone
// api.previewer.send( 'sek-refresh-stylesheet', {
// location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
// });
// Use the location_skope_id provided if set, otherwise generate it
var location_skope_id = params.location_skope_id;
if ( _.isUndefined( location_skope_id ) ) {
location_skope_id = true === params.is_global_location ? sektionsLocalizedData.globalSkopeId : api.czr_skopeBase.getSkopeProperty( 'skope_id' );
}
api.previewer.trigger('sek-refresh-stylesheet', {
//id : params.apiParams.location,
location_skope_id : location_skope_id,//<= send skope id to the preview so we can use it when ajaxing
is_global_location : self.isGlobalLocation( params.apiParams )
});
// Refresh when a section is created ( not duplicated )
if ( params.apiParams.is_first_section ) {
api.previewer.trigger( 'sek-refresh-level', {
level : 'location',
id : params.apiParams.location
});
}
// Remove the sektion_to_replace when dropping a preset_section in an empty section ( <= the one to replace )
if ( params.apiParams.sektion_to_replace ) {
api.previewer.trigger( 'sek-remove', {
id : params.apiParams.sektion_to_replace,
location : params.apiParams.location,
in_column : params.apiParams.in_column,//needed when removing a nested column
level : 'section'
});
}
// Refresh the stylesheet again after a delay
// For the moment, some styling, like fonts are not
// @todo fix => see why we need to do it.
// _.delay( function() {
// // Refresh the stylesheet to generate the css rules of the module
// api.previewer.send( 'sek-refresh-stylesheet', {
// location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
// });
// }, 1000 );
}
},//'sek-add-content-in-new-sektion'
// @params {
// drop_target_element : $(this),
// position : _position,
// before_section : $(this).data('sek-before-section'),
// after_section : $(this).data('sek-after-section'),
// content_type : event.originalEvent.dataTransfer.getData( "sek-content-type" ),
// content_id : event.originalEvent.dataTransfer.getData( "sek-content-id" )
// }
'sek-add-preset-section-in-new-nested-sektion' : {
callback : function( params ) {
sendToPreview = false;//<= when the level is refreshed when complete, we don't need to send to preview.
uiParams = {};
apiParams = params;
apiParams.action = 'sek-add-preset-section-in-new-nested-sektion';
api.previewer.send( 'sek-maybe-print-loader', { loader_located_in_level_id : params.location });
return self.updateAPISetting( apiParams );
},
complete : function( params ) {
// Always update the root fonts property after a module addition
// => because there might be a google font specified in the starting value or in a preset section
self.updateAPISetting({
action : 'sek-update-fonts',
is_global_location : self.isGlobalLocation( params.apiParams )
});
// Refresh the stylesheet to generate the css rules of the clone
// api.previewer.send( 'sek-refresh-stylesheet', {
// location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
// });
api.previewer.trigger('sek-refresh-stylesheet', {
id : params.apiParams.in_sektion,
location_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' )//<= send skope id to the preview so we can use it when ajaxing
});
api.previewer.trigger( 'sek-refresh-level', {
level : 'section',
id : params.apiParams.in_sektion
});
}
},
// GENERATE UI ELEMENTS
// June 2020 :
// When a user creates a new section, the content type switcher is set to section
// For all other cases, when user clicks on the + icon, the content type switcher is set to module
'sek-pick-content' : function( params ) {
params = _.isObject(params) ? params : {};
// Set the active content type here
// This is used in api.czrInputMap.content_type_switcher()
// Fixes issue https://github.com/presscustomizr/nimble-builder/issues/248
api.czr_sektions.currentContentPickerType = api.czr_sektions.currentContentPickerType || new api.Value();
// Set the last clicked target element id now => will be used for double click insertion of module / section
if ( _.isObject( params ) && params.id ) {
// self reset after a moment.
// @see CZRSeksPrototype::initialize
// implemented for https://github.com/presscustomizr/nimble-builder/issues/317
self.lastClickedTargetInPreview( { id : params.id } );
}
params = params || {};
sendToPreview = true;
apiParams = {};
uiParams = {
action : 'sek-generate-draggable-candidates-picker-ui',
content_type : params.content_type || 'module',
// <= the "was_triggered" param can be used to determine if we need to animate the picker control or not. @see ::generateUI() case 'sek-generate-draggable-candidates-picker-ui'
// true by default, because this is the most common scenario ( when adding a section, a column ... )
// but false when clicking on the + ui icon in the preview
was_triggered : _.has( params, 'was_triggered' ) ? params.was_triggered : true,
focus : _.has( params, 'focus' ) ? params.focus : true
};
return self.generateUI( uiParams );
},
'sek-edit-options' : function( params ) {
sendToPreview = true;
apiParams = {};
if ( _.isEmpty( params.id ) ) {
return $.Deferred( function() {
this.reject( 'missing id' );
});
}
uiParams = {
action : 'sek-generate-level-options-ui',
location : params.location,//<= added June 2020 for https://github.com/presscustomizr/nimble-builder-pro/issues/6
level : params.level,
id : params.id,
in_sektion : params.in_sektion,
in_column : params.in_column,
options : params.options || []
};
return self.generateUI( uiParams );
},
'sek-edit-module' : function( params ) {
sendToPreview = true;
apiParams = {};
uiParams = {
action : 'sek-generate-module-ui',
level : params.level,
id : params.id,
in_sektion : params.in_sektion,
in_column : params.in_column,
options : params.options || []
};
return self.generateUI( uiParams );
},
// OTHER MESSAGE TYPES
// @params {
// type : info, error, success
// notif_id : '',
// is_pro_notif: '',
// message : '',
// duration : in ms,
// button_see_me : true
// }
'sek-notify' : function( params ) {
sendToPreview = false;
var notif_id = params.notif_id || 'sek-notify';
params.button_see_me = _.isUndefined(params.button_see_me) ? true : params.button_see_me;
// Make sure we clean the last printed notification
if ( self.lastNimbleNotificationId ) {
api.notifications.remove( self.lastNimbleNotificationId );
}
return $.Deferred(function() {
api.panel( sektionsLocalizedData.sektionsPanelId, function( __main_panel__ ) {
api.notifications.add( new api.Notification( notif_id, {
type: params.type || 'info',
message: params.message,
dismissible: true
}));
self.lastNimbleNotificationId = notif_id;
var _doThingsWhenRendered = function() {
if ( params.is_pro_notif ) {
api.notifications( notif_id ).container.css('background', '#ffff88');
}
if ( params.button_see_me ) {
api.notifications( notif_id ).container.addClass('button-see-me-twice');
_.delay( function() {
api.notifications.container.removeClass('button-see-me-twice');
}, 2000 );
}
api.notifications.unbind('rendered', _doThingsWhenRendered );
};
if ( api.notifications.has( notif_id ) ) {
api.notifications.bind('rendered', _doThingsWhenRendered );
}
// Removed if not dismissed after 5 seconds
_.delay( function() {
api.notifications.remove( notif_id );
}, params.duration || 5000 );
});
// always pass the local or global skope of the currently customized location id when resolving the promise.
// It will be send to the preview and used when ajaxing
this.resolve({
is_global_location : self.isGlobalLocation( params )
});
});
},
'sek-refresh-level' : function( params ) {
sendToPreview = true;
return $.Deferred(function(_dfd_) {
apiParams = {
action : 'sek-refresh-level',
level : params.level,
id : params.id,
// added for https://github.com/presscustomizr/nimble-builder/issues/471
original_action : params.original_action,
moved_level_id : params.moved_level_id
};
uiParams = {};
// always pass the local or global skope of the currently customized location id when resolving the promise.
// It will be send to the preview and used when ajaxing
_dfd_.resolve({
is_global_location : self.isGlobalLocation( params )
});
});
},
'sek-refresh-stylesheet' : function( params ) {
sendToPreview = true;
params = params || {};
return $.Deferred(function(_dfd_) {
apiParams = {id : params.id};
uiParams = {};
// always pass the local or global skope of the currently customized location id when resolving the promise.
// It will be send to the preview and used when ajaxing
_dfd_.resolve({
is_global_location : self.isGlobalLocation( params )
});
});
},
// Updated June 2020 for https://github.com/presscustomizr/nimble-builder/issues/520
'sek-toggle-save-section-ui' : function( params ) {
sendToPreview = false;
self.idOfSectionToSave = params.id;
self.saveSectionDialogVisible( true );
return $.Deferred(function(_dfd_) {
apiParams = {
// action : 'sek-refresh-level',
// level : params.level,
// id : params.id
};
uiParams = {};
// always pass the local or global skope of the currently customized location id when resolving the promise.
// It will be send to the preview and used when ajaxing
_dfd_.resolve({
is_global_location : self.isGlobalLocation( params )
});
});
},
// RESET
'sek-reset-collection' : {
callback : function( params ) {
sendToPreview = false;//<= when the level is refreshed when complete, we don't need to send to preview.
uiParams = {};
apiParams = params;
apiParams.action = 'sek-reset-collection';
apiParams.scope = params.scope;
var _dfd_ = self.updateAPISetting( apiParams )
.done( function( resp) {
api.previewer.refresh();
api.previewer.trigger('sek-notify', {
notif_id : 'reset-success',
type : 'success',
duration : 8000,
message : [
'',
'',
sektionsLocalizedData.i18n['Reset complete'],
'',
''
].join('')
});
if ( 'local' === params.scope ) {
var _doThingsAfterRefresh = function() {
// INHERITANCE
// solves the problem of preventing group template inheritance after a local reset
var _is_inheritance_enabled_in_local_options = true,
currentSetValue = api( self.localSectionsSettingId() )(),
localOptions = currentSetValue.local_options;
if ( localOptions && _.isObject(localOptions) && localOptions.local_reset && !_.isUndefined( localOptions.local_reset.inherit_group_scope ) ) {
_is_inheritance_enabled_in_local_options = localOptions.local_reset.inherit_group_scope;
}
//api.infoLog('RESET MAIN LOCAL SETTING ON NEW SKOPES SYNCED', self.localSectionsSettingId() );
// Keep only the settings for global option, local options, content picker
// Remove all the others
// ( local options are removed below )
self.cleanRegisteredLevelSettings();
// Removes the local sektions setting
api.remove( self.localSectionsSettingId() );
// RE-register the local sektions setting with values sent from the server
// If the local page inherits a group skope, those will be set as local
// To prevent saving server sets property __inherits_group_skope_tmpl_when_exists__ = true
// set the param { dirty : true } => because otherwise, if user saves right after a reset, local option won't be ::updated() server side.
// Which means that the page will keep its previous aspect
try { self.setupSettingsToBeSaved( { dirty : true, is_group_inheritance_enabled : _is_inheritance_enabled_in_local_options } ); } catch( er ) {
api.errare( 'Error in self.localSectionsSettingId.callbacks => self.setupSettingsToBeSaved()' , er );
}
api.trigger('nimble-update-topbar-skope-status', { after_reset : true } );
// Removes and RE-register local settings and controls
self.generateUI({
action : 'sek-generate-local-skope-options-ui',
clean_settings_and_controls_first : true//<= see self.generateUIforLocalSkopeOptions()
});
// 'czr-new-skopes-synced' is always sent on a previewer.refresh()
api.previewer.unbind( 'czr-new-skopes-synced', _doThingsAfterRefresh );
};
api.previewer.bind( 'czr-new-skopes-synced', _doThingsAfterRefresh );
}//if ( 'local' === params.scope ) {
})
.fail( function( response ) {
api.errare( 'reset_button input => error when firing ::updateAPISetting', response );
api.previewer.trigger('sek-notify', {
notif_id : 'reset-failed',
type : 'error',
duration : 8000,
message : [
'',
'',
sektionsLocalizedData.i18n['Reset failed'],
' ',
'' + response + '',
'',
''
].join('')
});
});
return _dfd_;
},
complete : function( params ) {
// api.previewer.refresh();
// api.previewer.trigger('sek-notify', {
// notif_id : 'reset-success',
// type : 'success',
// duration : 8000,
// message : [
// '',
// '',
// sektionsLocalizedData.i18n['Reset complete'],
// '',
// ''
// ].join('')
// });
}
},
};//msgCollection
// Schedule the reactions
// May be send a message to the preview
_.each( msgCollection, function( callbackFn, msgId ) {
api.previewer.bind( msgId, function( params ) {
var _cb_;
if ( _.isFunction( callbackFn ) ) {
_cb_ = callbackFn;
} else if ( _.isFunction( callbackFn.callback ) ) {
_cb_ = callbackFn.callback;
} else {
api.errare( '::reactToPreviewMsg => invalid callback for action ' + msgId );
return;
}
// Close template gallery, template saver
// do nothing when we notify
if ( 'sek-notify' !== msgId ) {
self.templateGalleryExpanded(false);
self.tmplDialogVisible(false);
}
try { _cb_( params )
// the cloneId is passed when resolving the ::updateAPISetting() promise()
// they are needed on level duplication to get the newly generated level id.
.done( function( promiseParams ) {
promiseParams = promiseParams || {};
// Send to the preview
if ( sendToPreview ) {
var messageToSend = {
location_skope_id : true === promiseParams.is_global_location ? sektionsLocalizedData.globalSkopeId : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
local_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),
apiParams : apiParams,
uiParams : uiParams,
cloneId : ! _.isEmpty( promiseParams.cloneId ) ? promiseParams.cloneId : false
}, isError = false;
// when using api.previewer.send, the data are sent as a JSON ( see customize-base.js::send )
// If the object message to send has a circular reference, the JSON.stringify will break ( TypeError: Converting circular structure to JSON )
// fixes https://github.com/presscustomizr/nimble-builder/issues/848
try { JSON.stringify( messageToSend ); } catch( er ) {
api.errare( 'JSON.stringify problem when executing the callback of ' + msgId, messageToSend );
isError = true;
}
if ( ! isError ) {
api.previewer.send(
msgId,
messageToSend
);
}
} else {
// if nothing was sent to the preview, trigger the '*_done' action so we can execute the 'complete' callback
api.previewer.trigger( [ msgId, 'done' ].join('_'), { apiParams : apiParams, uiParams : uiParams } );
}
// say it
self.trigger( [ msgId, 'done' ].join('_'), params );
})
.fail( function( errorMsg ) {
api.errare( 'reactToPreviewMsg => problem or error when running action ' + msgId, errorMsg );
// api.panel( sektionsLocalizedData.sektionsPanelId, function( __main_panel__ ) {
// api.notifications.add( new api.Notification( 'sek-react-to-preview', {
// type: 'info',
// message: errorMsg,
// dismissible: true
// } ) );
// // Removed if not dismissed after 5 seconds
// _.delay( function() {
// api.notifications.remove( 'sek-react-to-preview' );
// }, 5000 );
// });
if ( !_.isEmpty( errorMsg ) && sektionsLocalizedData.isDevMode ) {
api.previewer.trigger('sek-notify', {
type : 'error',
duration : 30000,
message : [
'',
'' + errorMsg + '',
' ',
sektionsLocalizedData.i18n['If this problem locks Nimble Builder, you can try resetting the sections of this page.'],
' ',
'',
'',
'',
''
].join('')
});
}//if ( sektionsLocalizedData.isDevMode ) {
}); } catch( _er_ ) {
api.errare( 'reactToPreviewMsg => error when receiving ' + msgId, _er_ );
}
});
});
// Schedule actions when callback done msg is sent by the preview
_.each( msgCollection, function( callbackFn, msgId ) {
api.previewer.bind( [ msgId, 'done' ].join('_'), function( params ) {
if ( _.isFunction( callbackFn.complete ) ) {
try { callbackFn.complete( params ); } catch( _er_ ) {
api.errare( 'reactToPreviewMsg done => error when receiving ' + [msgId, 'done'].join('_') , _er_ );
}
}
});
});
},//reactToPreview();
// Fired in initialized on api(ready)
schedulePrintSectionJson : function() {
var self = this;
var popupCenter = function ( content ) {
w = 400;
h = 300;
// Fixes dual-screen position Most browsers Firefox
var dualScreenLeft = ! _.isUndefined( window.screenLeft ) ? window.screenLeft : window.screenX;
var dualScreenTop = ! _.isUndefined( window.screenTop ) ? window.screenTop : window.screenY;
var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
var left = ((width / 2) - (w / 2)) + dualScreenLeft;
var top = ((height / 2) - (h / 2)) + dualScreenTop;
var newWindow = window.open("about:blank", null, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
var doc = newWindow.document;
doc.open("text/html");
doc.write( content );
doc.close();
// Puts focus on the newWindow
if (window.focus) {
newWindow.focus();
}
};
api.previewer.bind( 'sek-to-json', function( params ) {
var sectionModel = $.extend( true, {}, self.getLevelModel( params.id ) );
console.log( JSON.stringify( self.cleanIds( sectionModel ) ) );
//popupCenter( JSON.stringify( cleanIds( sectionModel ) ) );
});
}//schedulePrintSectionJson
});//$.extend()
})( wp.customize, jQuery );//global sektionsLocalizedData
var CZRSeksPrototype = CZRSeksPrototype || {};
(function ( api, $ ) {
$.extend( CZRSeksPrototype, {
// @params = {
// action : 'sek-generate-module-ui' / 'sek-generate-level-options-ui'
// level : params.level,
// id : params.id,
// in_sektion : params.in_sektion,
// in_column : params.in_column,
// options : params.options || []
// }
// @return promise()
generateUI : function( params ) {
var self = this,
dfd = $.Deferred();
if ( _.isEmpty( params.action ) ) {
dfd.reject( 'generateUI => missing action' );
}
// REGISTER SETTING AND CONTROL
switch ( params.action ) {
// FRONT AND LEVEL MODULES UI
// The registered elements are cleaned (self.cleanRegisteredAndLargeSelectInput()) in the callbacks,
// because we want to check if the requested UI is not the one already rendered, and fire a button-see-me animation if yes.
case 'sek-generate-module-ui' :
try{ dfd = self.generateUIforFrontModules( params, dfd ); } catch( er ) {
api.errare( '::generateUI() => error', er );
dfd = $.Deferred();
}
break;
case 'sek-generate-level-options-ui' :
try{ dfd = self.generateUIforLevelOptions( params, dfd ); } catch( er ) {
api.errare( '::generateUI() => error', er );
dfd = $.Deferred();
}
break;
// Possible content types :
// 1) module
// 2) preset_section
case 'sek-generate-draggable-candidates-picker-ui' :
// Clean previously generated UI elements
self.cleanRegisteredAndLargeSelectInput();
try{ dfd = self.generateUIforDraggableContent( params, dfd ); } catch( er ) {
api.errare( '::generateUI() => error', er );
dfd = $.Deferred();
}
// June 2020 Make sure the content picker is set to "section" when user creates a new section
api.czr_sektions.currentContentPickerType( params.content_type || 'module' );
break;
// Fired in ::initialize()
case 'sek-generate-local-skope-options-ui' :
// Clean previously generated UI elements
self.cleanRegisteredAndLargeSelectInput();
try{ dfd = self.generateUIforLocalSkopeOptions( params, dfd ); } catch( er ) {
api.errare( '::generateUI() => error', er );
dfd = $.Deferred();
}
break;
// Fired in ::initialize()
case 'sek-generate-global-options-ui' :
// Clean previously generated UI elements
self.cleanRegisteredAndLargeSelectInput();
try{ dfd = self.generateUIforGlobalOptions( params, dfd ); } catch( er ) {
api.errare( '::generateUI() => error', er );
dfd = $.Deferred();
}
break;
}//switch
return 'pending' == dfd.state() ? dfd.resolve().promise() : dfd.promise();//<= we might want to resolve on focus.completeCallback ?
},//generateUI()
// @params = {
// uiParams : params,
// options_type : 'spacing',
// settingParams : {
// to : to,
// from : from,
// args : args
// }
// }
//
// @param settingParams.args = {
// inputRegistrationParams : {
// id :,
// type :
// refresh_markup : bool
// refresh_stylesheet : bool
// refresh_fonts : bool
// }
// input_changed : input_id
// input_transport : 'inherit'/'postMessage',
// module : { items : [...]}
// module_id :
// not_preview_sent : bool
//}
//
// Note 1 : this method must handle two types of modules :
// 1) mono item modules, for which the settingParams.to is an object, a single item object
// 2) multi-items modules, for which the settingParams.to is an array, a collection of item objects
// How do we know that we are a in single / multi item module ?
//
// Note 2 : we must also handle several scenarios of module value update :
// 1) mono-items and multi-items module => input change
// 2) crud multi item => item added or removed => in this case some args are not passed, like params.settingParams.args.inputRegistrationParams
updateAPISettingAndExecutePreviewActions : function( params ) {
if ( _.isEmpty( params.settingParams ) || !_.has( params.settingParams, 'to' ) ) {
api.errare( 'updateAPISettingAndExecutePreviewActions => missing params.settingParams.to. The api main setting can not be updated', params );
return;
}
var self = this;
// NORMALIZE THE VALUE WE WANT TO WRITE IN THE MAIN SETTING
// 1) We don't want to store the default title and id module properties
// 2) We don't want to write in db the properties that are set to their default values
var rawModuleValue = params.settingParams.to,
moduleValueCandidate,// {} or [] if mono item of multi-item module
parentModuleType = null,
isMultiItemModule = false;
if ( _.isEmpty( params.settingParams.args ) || !_.has( params.settingParams.args, 'moduleRegistrationParams' ) ) {
api.errare( 'updateAPISettingAndExecutePreviewActions => missing params.settingParams.args.moduleRegistrationParams The api main setting can not be updated', params );
return;
}
var _ctrl_ = params.settingParams.args.moduleRegistrationParams.control,
_module_id_ = params.settingParams.args.moduleRegistrationParams.id,
parentModuleInstance = _ctrl_.czr_Module( _module_id_ );
if ( !_.isEmpty( parentModuleInstance ) ) {
parentModuleType = parentModuleInstance.module_type;
isMultiItemModule = parentModuleInstance.isMultiItem();
} else {
api.errare( 'updateAPISettingAndExecutePreviewActions => missing parentModuleInstance', params );
}
// The new module value can be a single item object if monoitem module, or an array of item objects if multi-item crud
// Let's normalize it
if ( !isMultiItemModule && _.isObject( rawModuleValue ) ) {
moduleValueCandidate = self.normalizeAndSanitizeSingleItemInputValues( {
item_value : rawModuleValue,
parent_module_type : parentModuleType,
is_multi_items : false
});
} else {
moduleValueCandidate = [];
_.each( rawModuleValue, function( item ) {
moduleValueCandidate.push( self.normalizeAndSanitizeSingleItemInputValues( {
item_value :item,
parent_module_type : parentModuleType,
is_multi_items : true
}));
});
}
// WHAT TO REFRESH IN THE PREVIEW ? Markup, stylesheet, font ?
// The action to trigger is determined by the changed input
// For the options of a level, the default action is to refresh the stylesheet.
// But we might need to refresh the markup in some cases. Like for example when a css class is added. @see the boxed-wide layout example
if ( _.isEmpty( params.defaultPreviewAction ) ) {
api.errare( 'updateAPISettingAndExecutePreviewActions => missing defaultPreviewAction in passed params. No action can be triggered to the api.previewer.', params );
return;
}
// Set the default value
var refresh_stylesheet = 'refresh_stylesheet' === params.defaultPreviewAction,//<= default action for level options
refresh_markup = 'refresh_markup' === params.defaultPreviewAction,//<= default action for module options
refresh_fonts = 'refresh_fonts' === params.defaultPreviewAction,
refresh_preview = 'refresh_preview' === params.defaultPreviewAction,
refresh_css_via_post_message = false;//<= introduced for pro custom css
// Maybe set the input based value
var input_id = params.settingParams.args.input_changed;
var inputRegistrationParams;
// introduced when updating the new text editors
// https://github.com/presscustomizr/nimble-builder/issues/403
var refreshMarkupWhenNeededForInput = function() {
return inputRegistrationParams && _.isString( inputRegistrationParams.refresh_markup ) && 'true' !== inputRegistrationParams.refresh_markup && 'false' !== inputRegistrationParams.refresh_markup;
};
if ( !_.isUndefined( input_id ) ) {
inputRegistrationParams = self.getInputRegistrationParams( input_id, parentModuleType );
if ( !_.isUndefined( inputRegistrationParams.refresh_stylesheet ) ) {
refresh_stylesheet = Boolean( inputRegistrationParams.refresh_stylesheet );
}
if ( !_.isUndefined( inputRegistrationParams.refresh_markup ) ) {
if ( refreshMarkupWhenNeededForInput() ) {
refresh_markup = inputRegistrationParams.refresh_markup;
} else {
refresh_markup = Boolean( inputRegistrationParams.refresh_markup );
}
}
if ( !_.isUndefined( inputRegistrationParams.refresh_fonts ) ) {
refresh_fonts = Boolean( inputRegistrationParams.refresh_fonts );
}
if ( !_.isUndefined( inputRegistrationParams.refresh_preview ) ) {
refresh_preview = Boolean( inputRegistrationParams.refresh_preview );
}
if ( !_.isUndefined( inputRegistrationParams.refresh_css_via_post_message ) ) {
refresh_css_via_post_message = Boolean( inputRegistrationParams.refresh_css_via_post_message );
}
}
var _doUpdateWithRequestedAction = function() {
// GLOBAL OPTIONS CASE => SITE WIDE => WRITING IN A SPECIFIC OPTION, SEPARATE FROM THE SEKTION COLLECTION
if ( true === params.isGlobalOptions ) {
if ( _.isEmpty( params.options_type ) ) {
api.errare( 'updateAPISettingAndExecutePreviewActions => error when updating the global options => missing options_type');
return;
}
//api( sektionsLocalizedData.optNameForGlobalOptions )() is registered on ::initialize();
var rawGlobalOptions = api( sektionsLocalizedData.optNameForGlobalOptions )(),
clonedGlobalOptions = $.extend( true, {}, _.isObject( rawGlobalOptions ) ? rawGlobalOptions : {} ),
_valueCandidate = {};
// consider only the non empty settings for db
// booleans should bypass this check
_.each( moduleValueCandidate || {}, function( _val_, _key_ ) {
// Note : _.isEmpty( 5 ) returns true when checking an integer,
// that's why we need to cast the _val_ to a string when using _.isEmpty()
if ( !_.isBoolean( _val_ ) && _.isEmpty( _val_ + "" ) )
return;
_valueCandidate[ _key_ ] = _val_;
});
clonedGlobalOptions[ params.options_type ] = _valueCandidate;
// Set it
api( sektionsLocalizedData.optNameForGlobalOptions )( clonedGlobalOptions );
// REFRESH THE PREVIEW ?
if ( false !== refresh_preview ) {
api.previewer.refresh();
}
// Refresh the font list now, before ajax stylesheet update
// So that the .fonts collection is ready server side
if ( true === refresh_fonts ) {
var newFontFamily = params.settingParams.args.input_value;
if ( !_.isString( newFontFamily ) ) {
api.errare( 'updateAPISettingAndExecutePreviewActions => font-family must be a string', newFontFamily );
return;
}
// will add it only if gfont
self.updateGlobalGFonts( newFontFamily );
}
// REFRESH THE STYLESHEET ?
if ( true === refresh_stylesheet ) {
api.previewer.send( 'sek-refresh-stylesheet', {
local_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),
location_skope_id : sektionsLocalizedData.globalSkopeId
});
}
} else {
// LEVEL OPTION CASE => LOCAL
return self.updateAPISetting({
action : params.uiParams.action,// mandatory : 'sek-generate-level-options-ui', 'sek-generate-local-skope-options-ui',...
id : params.uiParams.id,
value : moduleValueCandidate,
in_column : params.uiParams.in_column,//not mandatory
in_sektion : params.uiParams.in_sektion,//not mandatory
// specific for level options and local skope options
options_type : params.options_type,// mandatory : 'layout', 'spacing', 'bg_border', 'height', ...
settingParams : params.settingParams
}).done( function( promiseParams ) {
// STYLESHEET => default action when modifying the level options
if ( true === refresh_stylesheet ) {
api.previewer.send( 'sek-refresh-stylesheet', {
location_skope_id : true === promiseParams.is_global_location ? sektionsLocalizedData.globalSkopeId : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
local_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
apiParams : {
action : 'sek-refresh-stylesheet',
id : params.uiParams.id,
level : params.uiParams.level
},
});
}
// MARKUP
// since https://github.com/presscustomizr/nimble-builder/issues/403, 2 cases :
// 1) update simply by postMessage, without ajax action <= refresh_markup is a string of selectors, and the content does not include content that needs server side parsing, like shortcode or template tages
// 2) otherwise => update the level with an ajax refresh action
var _changed_item_id;
if ( isMultiItemModule && params.settingParams.args.inputRegistrationParams && _.isFunction( params.settingParams.args.inputRegistrationParams.input_parent ) ) {
_changed_item_id = params.settingParams.args.inputRegistrationParams.input_parent.id;
}
var _sendRequestForAjaxMarkupRefresh = function() {
api.previewer.send( 'sek-refresh-level', {
location_skope_id : true === promiseParams.is_global_location ? sektionsLocalizedData.globalSkopeId : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
local_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
apiParams : {
action : 'sek-refresh-level',
id : params.uiParams.id,
level : params.uiParams.level,
changed_item_id : _changed_item_id,
control_id : _ctrl_.id,
is_multi_items : isMultiItemModule
},
skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
});
};
// NB ajaxily refreshes the markup
if ( true === refresh_markup ) {
_sendRequestForAjaxMarkupRefresh();
}
// Case when NB maybe refreshes the markup via postmessage
// Note : for multi-item modules, the changed item id is sent
if ( refreshMarkupWhenNeededForInput() ) {
var _html_content = params.settingParams.args.input_value;
if ( !_.isString( _html_content ) ) {
throw new Error( '::updateAPISettingAndExecutePreviewActions => _doUpdateWithRequestedAction => refreshMarkupWhenNeededForInput => html content is not a string.');
}
// Like shortcode tags, template tags, script tags
if ( !self.htmlIncludesElementsThatNeedAnAjaxRefresh( _html_content ) ) {
api.previewer.send( 'sek-update-html-in-selector', {
selector : inputRegistrationParams.refresh_markup,
changed_item_id : _changed_item_id,
is_multi_items : isMultiItemModule,
html : _html_content,
id : params.uiParams.id,
location_skope_id : true === promiseParams.is_global_location ? sektionsLocalizedData.globalSkopeId : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
local_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
apiParams : {
action : 'sek-update-html-in-selector',
id : params.uiParams.id,
level : params.uiParams.level
},
skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' )//<= send skope id to the preview so we can use it when ajaxing
});
} else {
_sendRequestForAjaxMarkupRefresh();
}
}
if ( true === refresh_css_via_post_message ) {
var _css_content = params.settingParams.args.input_value;
if ( !_.isString( _css_content ) ) {
throw new Error( '::updateAPISettingAndExecutePreviewActions => _doUpdateWithRequestedAction => refresh css with post message => css content is not a string.');
} else {
api.previewer.send( 'sek-update-css-with-postmessage', {
//selector : inputRegistrationParams.refresh_markup,
changed_item_id : _changed_item_id,
is_multi_items : isMultiItemModule,
css_content : _css_content,
id : params.uiParams.id,
location_skope_id : true === promiseParams.is_global_location ? sektionsLocalizedData.globalSkopeId : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
local_skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
apiParams : {
action : 'sek-update-css-with-postmessage',
id : params.uiParams.id,
level : params.uiParams.level
},
skope_id : api.czr_skopeBase.getSkopeProperty( 'skope_id' ),//<= send skope id to the preview so we can use it when ajaxing
is_current_page_custom_css : 'local_custom_css' === input_id
});
}
}
// REFRESH THE PREVIEW ?
if ( true === refresh_preview ) {
api.previewer.refresh();
}
})
.fail( function( er ) {
api.errare( '::updateAPISettingAndExecutePreviewActions=> api setting not updated', er );
api.errare( '::updateAPISettingAndExecutePreviewActions=> api setting not updated => params ', params );
});//self.updateAPISetting()
}
};//_doUpdateWithRequestedAction
// if the changed input is a google font modifier ( <=> true === refresh_fonts )
// => we want to first refresh the google font collection, and then proceed the requested action
// this way we make sure that the customized value used when ajaxing will be taken into account when writing the google font http request link
if ( true === refresh_fonts ) {
var newFontFamily = params.settingParams.args.input_value;
if ( !_.isString( newFontFamily ) ) {
api.errare( 'updateAPISettingAndExecutePreviewActions => font-family must be a string', newFontFamily );
return;
}
// add it only if gfont
if ( true === params.isGlobalOptions ) {
_doUpdateWithRequestedAction( newFontFamily );
} else {
self.updateAPISetting({
action : 'sek-update-fonts',
font_family : newFontFamily,
is_global_location : self.isGlobalLocation( params.uiParams )
})
// we use always() instead of done here, because the api section setting might not be changed ( and therefore return a reject() promise ).
// => this can occur when a user is setting a google font already picked elsewhere
// @see case 'sek-update-fonts'
.always( function() {
_doUpdateWithRequestedAction().then( function() {
// always refresh again after
// Why ?
// Because the first refresh was done before actually setting the new font family, so based on a previous set of fonts
// which leads to have potentially an additional google fonts that we don't need after the first refresh
// that's why this second refresh is required. It wont trigger any preview ajax actions. Simply refresh the root fonts property of the main api setting.
self.updateAPISetting({
action : 'sek-update-fonts',
is_global_location : self.isGlobalLocation( params.uiParams )
});
});
});
}
} else {
_doUpdateWithRequestedAction();
}
},//updateAPISettingAndExecutePreviewActions
// IMPORTANT => Updates the setting for global options
updateGlobalGFonts : function( newFontFamily ) {
var self = this;
//api( sektionsLocalizedData.optNameForGlobalOptions )() is registered on ::initialize();
var rawGlobalOptions = api( sektionsLocalizedData.optNameForGlobalOptions )(),
clonedGlobalOptions = $.extend( true, {}, _.isObject( rawGlobalOptions ) ? rawGlobalOptions : {} );
// Get the gfonts from the level options and modules values
var currentGfonts = self.sniffGlobalGFonts( clonedGlobalOptions );
// add it only if gfont
if ( !_.isEmpty( newFontFamily ) && _.isString( newFontFamily ) ) {
if ( newFontFamily.indexOf('gfont') > -1 && !_.contains( currentGfonts, newFontFamily ) ) {
currentGfonts.push( newFontFamily );
}
}
// update the global gfonts collection
// this is then used server side in Sek_Dyn_CSS_Handler::sek_get_gfont_print_candidates to build the Google Fonts request
clonedGlobalOptions.fonts = currentGfonts;
// Set it
api( sektionsLocalizedData.optNameForGlobalOptions )( clonedGlobalOptions );
},
// Walk the global option and populate an array of google fonts
// To be a candidate for sniffing, an input font value font should start with [gfont]
// @return array
sniffGlobalGFonts : function( _data_ ) {
var self = this,
gfonts = [],
_snifff_ = function( _data_ ) {
_.each( _data_, function( levelData, _key_ ) {
// of course, don't sniff the already stored fonts
if ( 'fonts' === _key_ )
return;
// example of input_id candidate 'font_family_css'
if ( _.isString( _key_ ) && _key_.indexOf('font_family') > -1 ) {
if ( levelData.indexOf('gfont') > -1 && !_.contains( gfonts, levelData ) ) {
gfonts.push( levelData );
}
}
if ( _.isArray( levelData ) || _.isObject( levelData ) ) {
_snifff_( levelData );
}
});
};
if ( _.isArray( _data_ ) || _.isObject( _data_ ) ) {
_snifff_( _data_ );
}
return gfonts;
},
// @return a normalized and sanitized item value
// What does this helper do ?
// 1) remove title and id properties for non multi-items modules, we don't need those properties in db
// 2) don't write if is equal to default
// @param params {
// item_value : rawModuleValue,
// parent_module_type : parentModuleType,
// is_multi_items : false
// }
normalizeAndSanitizeSingleItemInputValues : function( params ) {
var itemNormalized = {},
itemNormalizedAndSanitized = {},
inputDefaultValue = null,
inputType = null,
sanitizedVal,
self = this,
isEqualToDefault = function( _val, _default ) {
var equal = false;
if ( _.isBoolean( _val ) || _.isBoolean( _default ) ) {
equal = Boolean(_val) === Boolean(_default);
} else if ( _.isNumber( _val ) || _.isNumber( _default ) ) {
equal = Number( _val ) === Number( _default );
} else if ( _.isString( _val ) || _.isString( _default ) ) {
equal = _val+'' === _default+'';
} else if ( _.isObject( _val ) && _.isObject( _default ) ) {
equal = _.isEqual( _val,_default );
} else if ( _.isArray( _val ) && _.isArray( _default ) ) {
//@see https://stackoverflow.com/questions/39517316/check-for-equality-between-two-array
equal = JSON.stringify(_val.sort()) === JSON.stringify(_default.sort());
} else {
equal = _val === _default;
}
return equal;
};
// NORMALIZE
// title, id are always included in the defaultItemModel
// title and id are legacy entries that can be used in multi-items modules to identify and name the item
// we need the id to target each item when generating the CSS => @see https://github.com/presscustomizr/nimble-builder/issues/78
// For non multi-items modules, those properties don't need to be saved in database
// @see ::getDefaultItemModelFromRegisteredModuleData()
_.each( params.item_value, function( _val, input_id ) {
if ( 'title' === input_id )
return;
if ( !params.is_multi_items && 'id' === input_id )
return;
if ( null !== params.parent_module_type ) {
// Skip if the key is an "id" => specific to multi-item module, for which we have an id added in the js api and not registered in php.
if ( 'id' !== input_id ) {
inputDefaultValue = self.getInputDefaultValue( input_id, params.parent_module_type );
if ( 'no_default_value_specified' === inputDefaultValue ) {
api.infoLog( '::normalizeAndSanitizeSingleItemInputValues => missing default value for input ' + input_id + ' in module ' + params.parent_module_type );
}
}
}
if ( isEqualToDefault( _val, inputDefaultValue ) ) {
return;
// When the value is a string of an object, no need to write an empty value
} else if ( ( _.isString( _val ) || _.isObject( _val ) ) && _.isEmpty( _val ) ) {
return;
} else {
itemNormalized[ input_id ] = _val;
}
});
// SANITIZE
_.each( itemNormalized, function( _val, input_id ) {
// @see extend_api_base.js
// @see sektions::_7_0_sektions_add_inputs_to_api.js
switch( self.getInputType( input_id, params.parent_module_type ) ) {
case 'text' :
case 'textarea' :
case 'check' :
case 'gutencheck' :
case 'select' :
case 'radio' :
case 'number' :
case 'upload' :
case 'upload_url' :
case 'color' :
case 'wp_color_alpha' :
case 'wp_color' :
case 'content_picker' :
case 'detached_tinymce_editor' :
case 'nimble_tinymce_editor' :
case 'password' :
case 'range' :
case 'range_slider' :
case 'hidden' :
case 'h_alignment' :
case 'h_text_alignment' :
case 'spacing' :
case 'bg_position' :
case 'v_alignment' :
case 'font_size' :
case 'line_height' :
case 'font_picker' :
sanitizedVal = _val;
break;
default :
sanitizedVal = _val;
break;
}
itemNormalizedAndSanitized[ input_id ] = sanitizedVal;
});
return itemNormalizedAndSanitized;
},
// Is the UI currently displayed the one that is being requested ?
// If so, don't generate the ui again
// @return bool
isUIControlAlreadyRegistered : function( uiElementId ) {
var self = this,
uiCandidate = _.filter( self.registered(), function( registered ) {
return registered.id == uiElementId && 'control' === registered.what;
}),
controlIsAlreadyRegistered = false;
// If the control is not been tracked in our self.registered(), let's check if it is registered in the api
// Typically, the module / section picker will match that case, because we don't keep track of it ( so it's not cleaned )
if ( _.isEmpty( uiCandidate ) ) {
controlIsAlreadyRegistered = api.control.has( uiElementId );
} else {
controlIsAlreadyRegistered = true;
// we should have only one uiCandidate with this very id
if ( uiCandidate.length > 1 ) {
api.errare( 'isUIControlAlreadyRegistered => why is this control registered more than once ? => ' + uiElementId );
}
}
return controlIsAlreadyRegistered;
},
/**
* Gets a list of unique shortcodes or shortcode-look-alikes in the content.
*
* @param {string} content The content we want to scan for shortcodes.
*/
htmlIncludesElementsThatNeedAnAjaxRefresh : function( content ) {
if ( !_.isString( content ) )
return false;
content = content.replace(/\s+/g,'');//<= remove all spaces so that we can detect template tags and shortcodes that have spaces inside curly braces or bracket, like {{ the_tags }}
var shortcodes = content.match( /\[+([\w_-])+/g ),
tmpl_tags = content.match( /\{\{+([\w_-])+/g ),
// script detection introduced for https://github.com/presscustomizr/nimble-builder/issues/710
script_tags = content.match( /