//global sektionsLocalizedData //global serverControlParams (function ( api, $ ) { api.CZR_Helpers.getInputSubTemplate = function( template_name ) { if ( $('#tmpl-nimble-subtemplate___' + template_name ).length > 0 ) { return wp.template( 'nimble-subtemplate___' + template_name ); } else { api.errare( 'problem in api.czr_sektions.getInputSubTemplate(), missing js template in the DOM for template_name : ' + template_name ); return null; } }; var getInputTemplate = function( input_type ) { var template_name = input_type; switch( input_type ) { case 'czr_layouts' ://<= specific to the hueman theme case 'select' ://<= used in the customizr and hueman theme case 'simpleselect' ://<=used in Nimble Builder case 'fa_icon_picker' : case 'font_picker': case 'animation_picker' ://<= oct 2020 added for https://github.com/presscustomizr/nimble-builder-pro/issues/73 template_name = 'simpleselect'; break; case 'simpleselectWithDeviceSwitcher': template_name = 'simpleselect_deviceswitcher'; break; case 'multiselect': case 'category_picker': template_name = 'multiselect'; break; case 'h_alignment' : case 'horizAlignmentWithDeviceSwitcher' : template_name = 'h_alignment'; break; case 'h_text_alignment' : case 'horizTextAlignmentWithDeviceSwitcher' : template_name = 'h_text_alignment'; break; case 'range_simple' : case 'range_simple_device_switcher' : template_name = 'range_simple'; break; case 'number_simple' : template_name = 'number_simple'; break; case 'font_size' : case 'line_height' : case 'range_with_unit_picker' : case 'range_with_unit_picker_device_switcher' : template_name = 'range_with_unit_picker'; break; case 'spacing' : case 'spacingWithDeviceSwitcher' : template_name = 'spacing'; break; case 'upload' : case 'upload_url' : template_name = 'upload'; break; case 'bg_position' : case 'bgPositionWithDeviceSwitcher' : template_name = 'bg_position'; break; case 'verticalAlignWithDeviceSwitcher' : template_name = 'v_alignment'; break; } if ( $('#tmpl-nimble-input___' + template_name ).length > 0 ) { return wp.template( 'nimble-input___' + template_name ); } else { api.errare( 'problem in getInputTemplate(), missing js template in the DOM for input_type : ' + input_type ); return null; } }; // Overrides FMK method // @param args // control_id: "__nimble__51b2f35191b3__main_settings" // item_model: {id: "czr_heading_child_0", title: "", heading_text: "This is a heading.", heading_tag: "h1", h_alignment_css: {…}, …} // module_id: "__nimble__51b2f35191b3__main_settings_czr_module" // module_type: "czr_heading_child" // tmpl: "item-inputs" var originalMethod = api.CZR_Helpers.getModuleTmpl; api.CZR_Helpers.getModuleTmpl = function( args ) { args = _.extend( { tmpl : '', module_type: '', module_id : '', cache : true,//<= shall we cache the tmpl or not. Should be true in almost all cases. nonce: api.settings.nonce.save//<= do we need to set a specific nonce to fetch the tmpls ? }, args ); // target only Nimble modules // a nimble module id looks like : "__nimble__00b8efefe207_czr_module" if ( -1 === args.module_id.indexOf('__nimble__') ) { return originalMethod( args ); } var dfd = $.Deferred(); // are we good to go ? if ( _.isEmpty( args.tmpl ) || _.isEmpty( args.module_type ) ) { dfd.reject( 'api.CZR_Helpers.getModuleTmpl => missing tmpl or module_type param' ); } if ( ! api.czr_sektions.isModuleRegistered( args.module_type ) ) { dfd.reject( 'api.CZR_Helpers.getModuleTmpl => module type not registered' ); dfd.resolve(); return originalMethod( args ); } /// TEMP !! /// // dfd.resolve(); // return originalMethod( args ); if ( _.contains( [ // 'sek_content_type_switcher_module', // 'sek_module_picker_module' ], args.module_type ) ) { dfd.resolve(); return originalMethod( args ); } /// TEMP !! /// // Get the item input map var item_input_tmpls = api.czr_sektions.getRegisteredModuleProperty( args.module_type, 'tmpl' ); var item_input_map = ( _.isObject( item_input_tmpls ) && item_input_tmpls[ args.tmpl ] ) ? item_input_tmpls[ args.tmpl ] : {}; if ( _.isEmpty( item_input_map ) || !_.isObject( item_input_map ) ) { api.errare( 'getModuleTmpl => Error empty or invalid input map for module : ', args.module_type ); dfd.reject( 'getModuleTmpl => Error empty or invalid input map for module : ', args.module_type ); } // Get the item value var item_model = args.item_model, default_item_model = $.extend( true, {}, api.czr_sektions.getDefaultItemModelFromRegisteredModuleData( args.module_type ) ), cloned_default = $.extend( true, {},default_item_model ); // normalizes the item_model with defaults item_model = $.extend( cloned_default, item_model ); if ( _.isEmpty( item_model ) ) { api.errare( 'getModuleTmpl => Error invalid item model for module : ', args.module_type ); dfd.reject( 'getModuleTmpl => Error invalid item model for module : ', args.module_type ); } var input_html = '', input_type; // Loop on the item input map and render the input // the rendering uses a nested _ template mechanism // see https://stackoverflow.com/questions/8938841/underscore-js-nested-templates#13649447 var renderInputCollection = function( inputCollection ) { var _html = ''; _.each( inputCollection, function( input_data, input_id ){ input_type = input_data.input_type; // render generic input wrapper try { _html += wp.template( 'nimble-input-wrapper' )( { input_type : input_type, input_data : input_data, input_id : input_id, item_model : item_model, input_tmpl : getInputTemplate( input_type ), control_id : args.control_id //<= needed for some modules like tiny_mce_editor }); } catch( er ) { api.errare( 'getModuleTmpl => Error when parsing the nimble-input-wrapper template', er ); dfd.reject( 'getModuleTmpl => Error when parsing the nimble-input-wrapper template'); return false; } }); return _html; };//renderInputCollection // GENERATE MODULE HTML : two cases, with or without tabs if ( item_input_map.tabs ) { var _tabNavHtml = '', _tabContentHtml =''; _.each( item_input_map.tabs, function( rawTabData, tabKey ) { // normalizes var tabData = $.extend( true, {}, rawTabData ); tabData = $.extend( { inputs : {}, title : '' }, tabData ); // generate tab nav html var _attributes = !_.isEmpty( tabData.attributes ) ? tabData.attributes : ''; _tabNavHtml += '
  • ' + tabData.title +'
  • '; // 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 = ''; 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( /[\s\S]*?<\/script>/gi ), shortcode_result = [], tmpl_tag_result = []; if ( shortcodes ) { for ( var i = 0; i < shortcodes.length; i++ ) { var _shortcode = shortcodes[ i ].replace( /^\[+/g, '' ); if ( shortcode_result.indexOf( _shortcode ) === -1 ) { shortcode_result.push( _shortcode ); } } } if ( tmpl_tags ) { for ( var j = 0; j < tmpl_tags.length; j++ ) { var _tag = tmpl_tags[ j ].replace( /^\[+/g, '' ); if ( tmpl_tag_result.indexOf( _tag ) === -1 ) { tmpl_tag_result.push( _tag ); } } } return !_.isEmpty( shortcode_result ) || !_.isEmpty( tmpl_tag_result ) || !_.isEmpty( script_tags ); } });//$.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 || [] // } // @dfd = $.Deferred() // @return the state promise dfd generateUIforDraggableContent : function( params, dfd ) { var self = this; // Prepare the module map to register var registrationParams = {}; $.extend( registrationParams, { // The content type switcher has a priority lower than the other so it's printed on top // it's loaded last, because it needs to know the existence of all other sek_content_type_switcher_module : { settingControlId : sektionsLocalizedData.prefixForSettingsNotSaved + '_sek_content_type_switcher_ui', module_type : 'sek_content_type_switcher_module', controlLabel : self.getRegisteredModuleProperty( 'sek_content_type_switcher_module', 'name' ),//sektionsLocalizedData.i18n['Select a content type'], priority : 10, settingValue : { content_type : params.content_type } //icon : 'center_focus_weak' }, sek_module_picker_module : { settingControlId : sektionsLocalizedData.prefixForSettingsNotSaved + '_sek_draggable_modules_ui', module_type : 'sek_module_picker_module', controlLabel : self.getRegisteredModuleProperty( 'sek_module_picker_module', 'name' ),//sektionsLocalizedData.i18n['Pick a module'], content_type : 'module', priority : 20, icon : '' }, // June 2020 for : https://github.com/presscustomizr/nimble-builder/issues/520 // and https://github.com/presscustomizr/nimble-builder/issues/713 sek_my_sections_sec_picker_module : { settingControlId : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid() + '_sek_draggable_sections_ui', module_type : 'sek_my_sections_sec_picker_module', controlLabel : self.getRegisteredModuleProperty( 'sek_my_sections_sec_picker_module', 'name' ),//sektionsLocalizedData.i18n['My sections'], content_type : 'section', expandAndFocusOnInit : false, priority : 10, icon : '', is_new : false }, });// // Register prebuild sections modules // declared server side in inc/sektions/_front_dev_php/_constants_and_helper_functions/0_5_2_sektions_local_sektion_data.php // to understand how those sections are registered server side, see @see https://github.com/presscustomizr/nimble-builder/issues/713 _.each([ 'sek_intro_sec_picker_module', 'sek_features_sec_picker_module', 'sek_post_grids_sec_picker_module', 'sek_about_sec_picker_module', 'sek_contact_sec_picker_module', 'sek_team_sec_picker_module', 'sek_column_layouts_sec_picker_module', 'sek_header_sec_picker_module', 'sek_footer_sec_picker_module' ], function( mod_type, key ) { registrationParams[mod_type] = { settingControlId : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid() + '_sek_draggable_sections_ui', module_type : mod_type, controlLabel : self.getRegisteredModuleProperty( mod_type, 'name' ), content_type : 'section', expandAndFocusOnInit : 0 === key,//<= first section group is expanded on start priority : 30, icon : '', is_new : 'sek_post_grids_sec_picker_module' === mod_type }; }); //$.extend( registrationParams, { }); // Beta features to merge here ? // if ( sektionsLocalizedData.areBetaFeaturesEnabled ) { // $.extend( registrationParams, {}); // } // BAIL WITH A SEE-ME ANIMATION IF THIS UI IS CURRENTLY BEING DISPLAYED // Is the UI currently displayed the one that is being requested ? // If so : // 1) visually remind the user that a module should be dragged // 2) pass the content_type param to display the requested content_type var firstKey = _.keys( registrationParams )[0], firstControlId = registrationParams[firstKey].settingControlId; if ( self.isUIControlAlreadyRegistered( firstControlId ) ) { api.control( firstControlId, function( _control_ ) { _control_.focus({ completeCallback : function() { var $container = _control_.container; // @use button-see-mee css class declared in core in /wp-admin/css/customize-controls.css if ( $container.hasClass( 'button-see-me') ) return; $container.addClass('button-see-me'); _.delay( function() { $container.removeClass('button-see-me'); }, 800 ); } }); }); return dfd; }//if // @return void() _do_register_ = function() { _.each( registrationParams, function( optionData, optionType ){ if ( ! api.has( optionData.settingControlId ) ) { // synchronize the module setting with the main collection setting api( optionData.settingControlId, function( _setting_ ) { _setting_.bind( function( to, from ) { api.errare('generateUIforDraggableContent => the setting() should not changed'); }); }); api.CZR_Helpers.register( { origin : 'nimble', level : params.level, what : 'setting', id : optionData.settingControlId, dirty : false, value : optionData.settingValue || {}, transport : 'postMessage',// 'refresh', type : '_nimble_ui_'//will be dynamically registered but not saved in db as option// columnData.settingType }); } api.CZR_Helpers.register( { origin : 'nimble', level : params.level, what : 'control', id : optionData.settingControlId, label : optionData.controlLabel, type : 'czr_module',//sekData.controlType, module_type : optionData.module_type, section : self.SECTION_ID_FOR_CONTENT_PICKER, priority : optionData.priority || 10, settings : { default : optionData.settingControlId }, track : false//don't register in the self.registered() => this will prevent this container to be removed when cleaning the registered }).done( function() { api.control( optionData.settingControlId, function( _control_ ) { // set the control type property _control_.content_type = optionData.content_type;//<= used to handle visibility when switching content type with the "content_type_switcher" control // we set the focus to false when firing api.previewer.trigger( 'sek-pick-content', { focus : false }); in ::initialize() if ( true === params.focus ) { _control_.focus({ completeCallback : function() {} }); } var $title = _control_.container.find('label > .customize-control-title'), _titleContent = $title.html(); // We wrap the original text content in this span.sek-ctrl-accordion-title in order to style it (underlined) independently ( without styling the icons next to it ) $title.html( ['', _titleContent, '' ].join('') ); if ( optionData.is_new ) { var _titleHtml = $title.html(); $title.html( _titleHtml + ' New!' ); } // if this level has an icon, let's prepend it to the title if ( ! _.isUndefined( optionData.icon ) ) { $title.addClass('sek-flex-vertical-center').prepend( optionData.icon ); } // ACCORDION // Setup the accordion only for section content type if ( 'section' === _control_.content_type ) { // Hide the item wrapper // @see css _control_.container.attr('data-sek-expanded', "false" ); // prepend the animated arrow $title.prepend(''); // setup the initial state + initial click _control_.container.attr('data-sek-expanded', "false" ); if ( true === optionData.expandAndFocusOnInit && "false" == _control_.container.attr('data-sek-expanded' ) ) { //_control_.container.find('.czr-items-wrapper').show(); //$title.trigger('click'); _control_.container.addClass('sek-expand-on-init'); } } else { _control_.container.attr('data-sek-accordion', 'no'); } }); }); });//_.each api.trigger('nimble-modules-and-sections-controls-registered'); };//_do_register_ // the self.SECTION_ID_FOR_CONTENT_PICKER section is registered on initialize // @fixes https://github.com/presscustomizr/nimble-builder/issues/187 api.section( self.SECTION_ID_FOR_CONTENT_PICKER, function( _section_ ) { _do_register_(); // 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.find('.sek-level-option-icon').length < 1 ) { $sectionTitleEl.prepend( '' ); } // The default title looks like this : Customizing Title if ( 0 < $panelTitleEl.length && $panelTitleEl.find('.sek-level-option-icon').length < 1 ) { $panelTitleEl.find('.customize-action').after( '' ); } // Schedule the accordion behaviour self.scheduleModuleAccordion.call( _section_, { expand_first_control : false } ); _section_.container.find('.customize-control.sek-expand-on-init').find('label > .customize-control-title').trigger('click'); // Fetch the presetSectionCollection from the server now, so we save a few milliseconds when injecting the first preset_section // it populates api.nimble_ApiSections // // updated in v1.7.5, may 21st : performance improvements on customizer load // inserting preset sections is not on all Nimble sessions => let's only fetch when user inserts the first section // self._getApiSingleSectionData(); }); return dfd; } });//$.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 || [] // } // @dfd = $.Deferred() // @return the state promise dfd generateUIforFrontModules : function( params, dfd ) { var self = this; if ( _.isEmpty( params.id ) ) { dfd.reject( 'generateUI => missing id' ); } // For modules, we need to generate a UI for the module value var moduleValue = self.getLevelProperty({ property : 'value', id : params.id }); var moduleType = self.getLevelProperty({ property : 'module_type', id : params.id }); var moduleName = self.getRegisteredModuleProperty( moduleType, 'name' ); if ( _.isEmpty( moduleType ) ) { dfd.reject( 'generateUI => module => invalid module_type' ); } // Prepare the module map to register var modulesRegistrationParams = {}; if ( true === self.getRegisteredModuleProperty( moduleType, 'is_father' ) ) { var _childModules_ = self.getRegisteredModuleProperty( moduleType, 'children' ); if ( _.isEmpty( _childModules_ ) ) { throw new Error('::generateUIforFrontModules => a father module ' + moduleType + ' is missing children modules '); } else { _.each( _childModules_, function( mod_type, optionType ){ modulesRegistrationParams[ optionType ] = { settingControlId : params.id + '__' + optionType, module_type : mod_type, controlLabel : self.getRegisteredModuleProperty( mod_type, 'name' ) //icon : 'code' }; }); } } else { modulesRegistrationParams.__no_option_group_to_be_updated_by_children_modules__ = { settingControlId : params.id, module_type : moduleType, controlLabel : moduleName //icon : 'code' }; } // BAIL WITH A SEE-ME ANIMATION IF THIS UI IS CURRENTLY BEING DISPLAYED // Is the UI currently displayed the one that is being requested ? // Check if the first control of the list is already registered // If so, visually remind the user and break; var firstKey = _.keys( modulesRegistrationParams )[0], firstControlId = modulesRegistrationParams[firstKey].settingControlId; if ( self.isUIControlAlreadyRegistered( firstControlId ) ) { api.control( firstControlId ).focus({ completeCallback : function() { var $container = api.control( firstControlId ).container; // @use button-see-mee css class declared in core in /wp-admin/css/customize-controls.css if ( $container.hasClass( 'button-see-me') ) return; $container.addClass('button-see-me'); _.delay( function() { $container.removeClass('button-see-me'); }, 800 ); } }); return dfd; }//if // Clean previously generated UI elements self.cleanRegisteredAndLargeSelectInput(); _do_register_ = function() { _.each( modulesRegistrationParams, function( optionData, optionType ){ // Make sure this setting is bound only once ! if ( ! api.has( optionData.settingControlId ) ) { var doUpdate = function( to, from, args ) { try { self.updateAPISettingAndExecutePreviewActions({ defaultPreviewAction : 'refresh_markup', uiParams : _.extend( params, { action : 'sek-set-module-value' } ), options_type : optionType, settingParams : { to : to, from : from, args : args } }); } catch( er ) { api.errare( '::generateUIforFrontModules => Error in updateAPISettingAndExecutePreviewActions', er ); } }; // Schedule the binding to synchronize the module setting with the main collection setting // Note 1 : unlike control or sections, the setting are not getting cleaned up on each ui generation. // They need to be kept in order to keep track of the changes in the customizer. // => that's why we check if ! api.has( ... ) api( optionData.settingControlId, function( _setting_ ) { _setting_.bind( _.debounce( doUpdate, self.SETTING_UPDATE_BUFFER ) );//_setting_.bind( _.debounce( function( to, from, args ) {} }); var settingValueOnRegistration = $.extend( true, {}, moduleValue ); if ( '__no_option_group_to_be_updated_by_children_modules__' !== optionType ) { settingValueOnRegistration = ( !_.isEmpty( settingValueOnRegistration ) && _.isObject( settingValueOnRegistration ) && _.isObject( settingValueOnRegistration[optionType] ) ) ? settingValueOnRegistration[optionType] : {}; } api.CZR_Helpers.register({ origin : 'nimble', level : params.level, what : 'setting', id : optionData.settingControlId, dirty : false, value : settingValueOnRegistration, transport : 'postMessage',// 'refresh', type : '_nimble_ui_'//will be dynamically registered but not saved in db as option// columnData.settingType }); }//if ( ! api.has( optionData.settingControlId ) ) api.CZR_Helpers.register( { origin : 'nimble', level : params.level, what : 'control', module_id : params.id,// <= the id of the corresponding module level as saved in DB. Can be needed, @see image slider module id : optionData.settingControlId, label : optionData.controlLabel, //label : sektionsLocalizedData.i18n['Customize the options for module :'] + ' ' + optionData.controlLabel, type : 'czr_module',//sekData.controlType, module_type : optionData.module_type, section : params.id, priority : 20, settings : { default : optionData.settingControlId } }).done( function() {}); // Implement the animated arrow markup, and the initial state of the module visibility api.control( optionData.settingControlId, function( _control_ ) { api.control( optionData.settingControlId ).focus({ completeCallback : function() {} }); // Hide the item wrapper // @see css _control_.container.attr('data-sek-expanded', "false" ); var $title = _control_.container.find('label > .customize-control-title'), // store the title text in a var + decode html entities ( added by WP ) // @see https://stackoverflow.com/questions/1147359/how-to-decode-html-entities-using-jquery _titleContent = $("
    ").html( $title.html() ).text(); $title.html( ['', _titleContent , '' ].join('') ); // if this level has an icon, let's prepend it to the title if ( ! _.isUndefined( optionData.icon ) ) { $title.addClass('sek-flex-vertical-center').prepend( optionData.icon ); } // prepend the animated arrow $title.prepend(''); // setup the initial state + initial click _control_.container.attr('data-sek-expanded', "false" ); }); });//each() };//_do_register() // Defer the registration when the parent section gets added to the api api.section( params.id, function( _section_ ) { api.section(params.id).focus(); // Generate the UI for module option switcher // introduded in july 2019 for https://github.com/presscustomizr/nimble-builder/issues/135 self.generateModuleOptionSwitcherUI( params.id, params.action ); _do_register_(); // don't display the clickable section title in the nimble root panel _section_.container.find('.accordion-section-title').first().hide(); // Style the section title var $panelTitleEl = _section_.container.find('.customize-section-title h3'); // 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_, { expand_first_control : true } ); }); // Register the module content section api.CZR_Helpers.register({ origin : 'nimble', what : 'section', id : params.id, title: sektionsLocalizedData.i18n['Content for'] + ' ' + moduleName, panel : sektionsLocalizedData.sektionsPanelId, priority : 1000, //track : false//don't register in the self.registered() //constructWith : MainSectionConstructor, }).done( function() {}); return dfd; }, // Generate the UI for module option switcher // introduded in july 2019 for https://github.com/presscustomizr/nimble-builder/issues/135 // REGISTER MODULE OPTION SWITCHER SETTING AND CONTROL generateModuleOptionSwitcherUI : function( module_id, ui_action ) { var setCtrlId = module_id + '__' + 'option_switcher'; if ( ! api.has( setCtrlId ) ) { // synchronize the module setting with the main collection setting api( setCtrlId, function( _setting_ ) { _setting_.bind( function( to, from ) { api.errare('generateUIforDraggableContent => the setting() should not changed'); }); }); api.CZR_Helpers.register( { origin : 'nimble', level : 'module', what : 'setting', id : setCtrlId, dirty : false, value : '', transport : 'postMessage',// 'refresh', type : '_nimble_ui_'//will be dynamically registered but not saved in db as option// columnData.settingType }); } api.CZR_Helpers.register( { origin : 'nimble', level : 'module', what : 'control', module_id : module_id,// <= the id of the corresponding module level as saved in DB id : setCtrlId, label : '', type : 'czr_module',//sekData.controlType, module_type : 'sek_mod_option_switcher_module', section : module_id, priority : 10, settings : { default : setCtrlId }, has_accordion : false, ui_action : ui_action // 'sek-generate-module-ui' or 'sek-generate-level-options-ui' // <= will be used to determine which button is selected }).done( function() { api.control( setCtrlId, function( _control_ ) { _control_.deferred.embedded.done( function() { // Hide the control label _control_.container.find('.customize-control-title').hide(); // don't setup the accordion _control_.container.attr('data-sek-accordion', 'no'); }); }); }); }//generateModuleOptionSwitcherUI });//$.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 || [] // } // @dfd = $.Deferred() // @return the state promise dfd generateUIforLevelOptions : function( params, dfd ) { var self = this; // Get this level options var levelOptionValues = self.getLevelProperty({ property : 'options', id : params.id }); levelOptionValues = _.isObject( levelOptionValues ) ? levelOptionValues : {}; // Prepare the module map to register var modulesRegistrationParams = {}; // June 2020 : introduced for https://github.com/presscustomizr/nimble-builder-pro/issues/6 // so we can remotely register modules api.trigger('nb_setup_level_ui_registration_params', { params : params, modulesRegistrationParams : modulesRegistrationParams }); $.extend( modulesRegistrationParams, { bg : { settingControlId : params.id + '__bg_options', module_type : 'sek_level_bg_module', controlLabel : sektionsLocalizedData.i18n['Background settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], expandAndFocusOnInit : true, icon : 'gradient'//'brush' } }); // implemented for https://github.com/presscustomizr/nimble-builder/issues/504 if ( 'section' === params.level ) { $.extend( modulesRegistrationParams, { level_text : { settingControlId : params.id + '__text_options', module_type : 'sek_level_text_module', controlLabel : sektionsLocalizedData.i18n['Text settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], expandAndFocusOnInit : true, icon : 'text_format'//'brush' } }); } $.extend( modulesRegistrationParams, { border : { settingControlId : params.id + '__border_options', module_type : 'sek_level_border_module', controlLabel : sektionsLocalizedData.i18n['Borders settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], //expandAndFocusOnInit : true, icon : 'rounded_corner'//'brush' }, spacing : { settingControlId : params.id + '__spacing_options', module_type : 'column' === params.level ? 'sek_level_spacing_module_for_columns' : 'sek_level_spacing_module', controlLabel : sektionsLocalizedData.i18n['Padding and margin settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], icon : 'center_focus_weak' }, anchor : { settingControlId : params.id + '__anchor_options', module_type : 'sek_level_anchor_module', controlLabel : sektionsLocalizedData.i18n['Custom anchor ( CSS ID ) and CSS classes for the'] + ' ' + sektionsLocalizedData.i18n[params.level], icon : '' }, visibility : { settingControlId : params.id + '__visibility_options', module_type : 'sek_level_visibility_module', controlLabel : sektionsLocalizedData.i18n['Device visibility settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], icon : '' }, height : { settingControlId : params.id + '__height_options', module_type : 'sek_level_height_module', controlLabel : sektionsLocalizedData.i18n['Height, vertical alignment, z-index for the'] + ' ' + sektionsLocalizedData.i18n[params.level], icon : '' }, }); if ( sektionsLocalizedData.isUpsellEnabled || sektionsLocalizedData.isPro ) { $.extend( modulesRegistrationParams, { animation : { settingControlId : params.id + '__animate_options', module_type : 'sek_level_animation_module', controlLabel : sektionsLocalizedData.i18n['Animation settings for the']+ ' ' + sektionsLocalizedData.i18n[params.level], icon : 'movie_filter', isPro : true } }); } if ( 'section' === params.level ) { $.extend( modulesRegistrationParams, { width : { settingControlId : params.id + '__width_options', module_type : 'sek_level_width_section', controlLabel : sektionsLocalizedData.i18n['Width settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], icon : '' } }); // Deactivated // => replaced by sek_level_width_section // $.extend( modulesRegistrationParams, { // layout : { // settingControlId : params.id + '__sectionLayout_options', // module_type : 'sek_level_section_layout_module', // controlLabel : sektionsLocalizedData.i18n['Layout settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], // icon : 'crop_din' // } // }); // Pro icon $.extend( modulesRegistrationParams, { breakpoint : { settingControlId : params.id + '__breakpoint_options', module_type : 'sek_level_breakpoint_module', controlLabel : sektionsLocalizedData.i18n['Responsive settings : breakpoint, column direction'], icon : 'devices' } }); } if ( 'column' === params.level ) { $.extend( modulesRegistrationParams, { width : { settingControlId : params.id + '__width_options', module_type : 'sek_level_width_column', controlLabel : sektionsLocalizedData.i18n['Width settings for the'] + ' ' + sektionsLocalizedData.i18n[params.level], icon : '' } }); } if ( 'module' === params.level ) { $.extend( modulesRegistrationParams, { width : { settingControlId : params.id + '__width_options', module_type : 'sek_level_width_module', controlLabel : sektionsLocalizedData.i18n['Width and horizontal alignment for the'] + ' ' + sektionsLocalizedData.i18n[params.level], icon : '' } }); } if ( sektionsLocalizedData.isUpsellEnabled || sektionsLocalizedData.isPro ) { $.extend( modulesRegistrationParams, { level_cust_css : { settingControlId : params.id + '__level_custom_css', module_type : 'sek_level_cust_css_level', controlLabel : sektionsLocalizedData.i18n['Custom CSS'], icon : 'code', isPro : true } }); } // BAIL WITH A SEE-ME ANIMATION IF THIS UI IS CURRENTLY BEING DISPLAYED // Is the UI currently displayed the one that is being requested ? // Check if the first control of the list is already registered // If so, visually remind the user and break; var firstKey = _.keys( modulesRegistrationParams )[0], firstControlId = modulesRegistrationParams[firstKey].settingControlId; if ( self.isUIControlAlreadyRegistered( firstControlId ) ) { api.control( firstControlId ).focus({ completeCallback : function() { var $container = api.control( firstControlId ).container; // @use button-see-mee css class declared in core in /wp-admin/css/customize-controls.css if ( $container.hasClass( 'button-see-me') ) return; $container.addClass('button-see-me'); _.delay( function() { $container.removeClass('button-see-me'); }, 800 ); } }); return dfd; }//if // Clean previously generated UI elements self.cleanRegisteredAndLargeSelectInput(); // @return void() _do_register_ = function() { _.each( modulesRegistrationParams, function( optionData, optionType ){ // Is the UI currently displayed the one that is being requested ? // If so, don't generate the ui again, simply focus on the section if ( self.isUIControlAlreadyRegistered( optionData.settingControlId ) ) { api.section( api.control( optionData.settingControlId ).section() ).expanded( true ); return; } if( ! api.has( optionData.settingControlId ) ) { var doUpdate = function( to, from, args ) { try { self.updateAPISettingAndExecutePreviewActions({ defaultPreviewAction : 'refresh_stylesheet', uiParams : params, options_type : optionType,// <= this is the options sub property where we will store this setting values. @see updateAPISetting case 'sek-generate-level-options-ui' settingParams : { to : to, from : from, args : args } }); } catch( er ) { api.errare( '::_do_register_ => Error in updateAPISettingAndExecutePreviewActions', er ); } }; // Schedule the binding to synchronize the options with the main collection setting // Note 1 : unlike control or sections, the setting are not getting cleaned up on each ui generation. // They need to be kept in order to keep track of the changes in the customizer. // => that's why we check if ! api.has( ... ) api( optionData.settingControlId, function( _setting_ ) { _setting_.bind( _.debounce( doUpdate, self.SETTING_UPDATE_BUFFER ) );//_setting_.bind( _.debounce( function( to, from, args ) {} });//api( Id, function( _setting_ ) {}) // Let's add the starting values if provided when registrating the module var initialModuleValues = levelOptionValues[ optionType ] || {}; var startingModuleValue = self.getModuleStartingValue( optionData.module_type ); if ( 'no_starting_value' !== startingModuleValue && _.isObject( startingModuleValue ) ) { // make sure the starting values are deeped clone now, before being extended var clonedStartingModuleValue = $.extend( true, {}, startingModuleValue ); initialModuleValues = $.extend( clonedStartingModuleValue, initialModuleValues ); } api.CZR_Helpers.register( { origin : 'nimble', level : params.level, what : 'setting', id : optionData.settingControlId, dirty : false, value : initialModuleValues, transport : 'postMessage',// 'refresh', type : '_nimble_ui_'//will be dynamically registered but not saved in db as option //sekData.settingType }); }//if( ! api.has( optionData.settingControlId ) ) { api.CZR_Helpers.register( { origin : 'nimble', level : params.level, level_id : params.id, what : 'control', id : optionData.settingControlId, label : optionData.controlLabel, type : 'czr_module',//sekData.controlType, module_type : optionData.module_type, section : params.id, priority : 20, settings : { default : optionData.settingControlId } }).done( function() {}); // Implement the animated arrow markup, and the initial state of the module visibility api.control( optionData.settingControlId, function( _control_ ) { if ( true === optionData.expandAndFocusOnInit ) { _control_.focus({ completeCallback : function() {} }); } // Hide the item wrapper // @see css _control_.container.attr('data-sek-expanded', "false" ); var $title = _control_.container.find('label > .customize-control-title'), _titleContent = $title.html(); // We wrap the original text content in this span.sek-ctrl-accordion-title in order to style it (underlined) independently ( without styling the icons next to it ) $title.html( ['', _titleContent, '' ].join('') ); // if this level has an icon, let's prepend it to the title if ( !_.isUndefined( optionData.icon ) ) { $title.addClass('sek-flex-vertical-center').prepend( optionData.icon ); } // prepend the animated arrow $title.prepend(''); // if this section is pro => add the icon if ( optionData.isPro ) { $title.append( [ '', ].join('') ); } // setup the initial state + initial click _control_.container.attr('data-sek-expanded', "false" ); if ( true === optionData.expandAndFocusOnInit && "false" == _control_.container.attr('data-sek-expanded' ) ) { $title.trigger('click'); } }); });//_.each() };//_do_register_() // - Defer the registration when the parent section gets added to the api // - Implement the module visibility api.section( params.id, function( _section_ ) { _do_register_(); // Generate the UI for module option switcher // introduded in july 2019 for https://github.com/presscustomizr/nimble-builder/issues/135 if ( 'module' === params.level ) { self.generateModuleOptionSwitcherUI( params.id, params.action ); } // don't display the clickable section title in the nimble root panel _section_.container.find('.accordion-section-title').first().hide(); // Style the section title var $panelTitleEl = _section_.container.find('.customize-section-title h3'); // The default title looks like this : Customizing Title if ( 0 < $panelTitleEl.length && $panelTitleEl.find('.sek-level-option-icon').length < 1 ) { $panelTitleEl.find('.customize-action').after( '' ); } // Schedule the accordion behaviour self.scheduleModuleAccordion.call( _section_, { expand_first_control : false } ); }); // Register the level settings section api.CZR_Helpers.register({ origin : 'nimble', what : 'section', id : params.id, title: sektionsLocalizedData.i18n['Settings for the'] + ' ' + params.level, panel : sektionsLocalizedData.sektionsPanelId, priority : 10, //track : false//don't register in the self.registered() //constructWith : MainSectionConstructor, }).done( function() {}); return dfd; } });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { getLocalSkopeOptionId : function() { var skope_id = api.czr_skopeBase.getSkopeProperty( 'skope_id' ); if ( _.isEmpty( skope_id ) ) { api.errare( 'czr_sektions::getLocalSkopeOptionId => empty skope_id '); return ''; } return sektionsLocalizedData.prefixForSettingsNotSaved + skope_id + '__localSkopeOptions'; }, // @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 || [] // } // @dfd = $.Deferred() // @return the state promise dfd generateUIforLocalSkopeOptions : function( params, dfd ) { var self = this, _id_ = self.getLocalSkopeOptionId(); // Is the UI currently displayed the one that is being requested ? // If so, visually remind the user that a module should be dragged if ( self.isUIControlAlreadyRegistered( _id_ ) ) { return dfd; } // Prepare the module map to register self.localOptionsRegistrationParams = {}; if ( _.isUndefined( sektionsLocalizedData.localOptionsMap ) || ! _.isObject( sektionsLocalizedData.localOptionsMap ) ) { api.errare( '::generateUIforGlobalOptions => missing or invalid localOptionsMap'); return dfd; } // remove settings when requested // Happens when // - importing a file // - after a local reset // - after a template injection // - a history navigation action if ( true === params.clean_settings_and_controls_first ) { self.cleanRegisteredLocalOptionSettingsAndControls(); } // Populate the registration params _.each( sektionsLocalizedData.localOptionsMap, function( mod_type, opt_name ) { switch( opt_name ) { case 'template' : self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__template', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Page template'], expandAndFocusOnInit : false, icon : 'check_box_outline_blank' }; break; // Header and footer have been beta tested during 5 months and released in June 2019, in version 1.8.0 case 'local_header_footer': self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__local_header_footer', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Page header and footer'], icon : 'web' }; break; case 'widths' : self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__widths', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Inner and outer widths'], icon : '' }; break; case 'custom_css' : self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__custom_css', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Custom CSS'], icon : 'code' }; break; case 'local_performances' : self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__local_performances', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Page speed optimizations'], icon : 'network_check' }; break; case 'local_reset' : self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__local_reset', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Remove all sections and options of this page'], icon : 'delete' }; break; case 'local_revisions' : self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__local_revisions', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Revision history of local sections'], icon : 'history' }; break; case 'import_export' : self.localOptionsRegistrationParams[ opt_name ] = { settingControlId : _id_ + '__local_imp_exp', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Export / Import'], icon : 'import_export' }; break; default : api.errare('::generateUIforLocalOptions => an option group could not be registered => ' + mod_type, opt_name ); break; }//switch });//_.each // Get the current local options from the local setting value // local setting value is structured this way : // { // collection : [], // local_options : {}, // fonts : [] // } // we only need the local_options here var currentSetValue = api( self.localSectionsSettingId() )(), currentAllLocalOptionsValue = $.extend( true, {}, _.isObject( currentSetValue.local_options ) ? currentSetValue.local_options : {} ); _do_register_ = function() { _.each( self.localOptionsRegistrationParams, function( optionData, optionType ){ // Let's add the starting values if provided when registrating the module var startingModuleValue = self.getModuleStartingValue( optionData.module_type ), optionTypeValue = _.isObject( currentAllLocalOptionsValue[ optionType ] ) ? currentAllLocalOptionsValue[ optionType ]: {}, initialModuleValues = optionTypeValue; // SETTING if ( !api.has( optionData.settingControlId ) ) { var doUpdate = function( to, from, args ) { try { self.updateAPISettingAndExecutePreviewActions({ defaultPreviewAction : 'refresh_preview', uiParams : params, options_type : optionType, settingParams : { to : to, from : from, args : args } }); } catch( er ) { api.errare( '::generateUIforLocalSkopeOptions => Error in updateAPISettingAndExecutePreviewActions', er ); } }; // Schedule the binding to synchronize the options with the main collection setting // Note 1 : unlike control or sections, the setting are not getting cleaned up on each ui generation. // They need to be kept in order to keep track of the changes in the customizer. // => that's why we check if ! api.has( ... ) api( optionData.settingControlId, function( _setting_ ) { _setting_.bind( _.debounce( doUpdate, self.SETTING_UPDATE_BUFFER ) );//_setting_.bind( _.debounce( function( to, from, args ) {} });//api( Id, function( _setting_ ) {}) if ( 'no_starting_value' !== startingModuleValue && _.isObject( startingModuleValue ) ) { // make sure the starting values are deeped clone now, before being extended var clonedStartingModuleValue = $.extend( true, {}, startingModuleValue ); initialModuleValues = $.extend( clonedStartingModuleValue, initialModuleValues ); } api.CZR_Helpers.register( { origin : 'nimble', level : params.level, what : 'setting', id : optionData.settingControlId, dirty : false, value : initialModuleValues, transport : 'postMessage',//'refresh',//// , type : '_nimble_ui_'//will be dynamically registered but not saved in db as option// columnData.settingType }); }//if ( ! api.has( optionData.settingControlId ) ) // CONTROL if ( !api.control.has( optionData.settingControlId ) ) { api.CZR_Helpers.register({ origin : 'nimble', level : params.level, what : 'control', id : optionData.settingControlId, label : optionData.controlLabel, type : 'czr_module',//sekData.controlType, module_type : optionData.module_type, section : self.SECTION_ID_FOR_LOCAL_OPTIONS, priority : 10, settings : { default : optionData.settingControlId }, track : true//don't register in the self.registered() => this will prevent this container to be removed when cleaning the registered }).done( function() { // if ( true === optionData.expandAndFocusOnInit ) { // api.control( optionData.settingControlId ).focus({ // completeCallback : function() {} // }); // } // Implement the animated arrow markup, and the initial state of the module visibility api.control( optionData.settingControlId, function( _control_ ) { // Hide the item wrapper // @see css _control_.container.attr('data-sek-expanded', "false" ); var $title = _control_.container.find('label > .customize-control-title').first(), _titleContent = $title.html(); // We wrap the original text content in this span.sek-ctrl-accordion-title in order to style it (underlined) independently ( without styling the icons next to it ) $title.html( ['', _titleContent, '' ].join('') ); // if this level has an icon, let's prepend it to the title if ( ! _.isUndefined( optionData.icon ) ) { $title.addClass('sek-flex-vertical-center').prepend( optionData.icon ); } // prepend the animated arrow $title.prepend(''); // setup the initial state + initial click _control_.container.attr('data-sek-expanded', "false" ); if ( true === optionData.expandAndFocusOnInit && "false" == _control_.container.attr('data-sek-expanded' ) ) { $title.trigger('click'); } }); }); } });//_.each() };//_do_register() // The parent section has already been added in ::initialize() _do_register_(); return dfd; } });//$.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 || [] // } // @dfd = $.Deferred() // @return the state promise dfd generateUIforGlobalOptions : function( params, dfd ) { var self = this, _id_ = sektionsLocalizedData.prefixForSettingsNotSaved + sektionsLocalizedData.optNameForGlobalOptions; // Is the UI currently displayed the one that is being requested ? // If so, visually remind the user that a module should be dragged if ( self.isUIControlAlreadyRegistered( _id_ ) ) { return dfd; } // Prepare the module map to register var registrationParams = {}; if ( _.isUndefined( sektionsLocalizedData.globalOptionsMap ) || ! _.isObject( sektionsLocalizedData.globalOptionsMap ) ) { api.errare( '::generateUIforGlobalOptions => missing or invalid globalOptionsMap'); return dfd; } // Populate the registration params _.each( sektionsLocalizedData.globalOptionsMap, function( mod_type, opt_name ) { switch( opt_name ) { case 'site_templates' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__site_templates', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Site templates'], icon : 'devices' }; break; // Header and footer have been beta tested during 5 months and released in June 2019, in version 1.8.0 case 'global_header_footer': registrationParams[ opt_name ] = { settingControlId : _id_ + '__header_footer', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Site wide header and footer'], icon : 'web' }; break; case 'global_text' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__global_text', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Global text options for Nimble sections'], icon : 'text_format' }; break; case 'widths' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__widths', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Site wide inner and outer sections widths'], icon : '' }; break; case 'breakpoint' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__breakpoint', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Site wide breakpoint for Nimble sections'], expandAndFocusOnInit : false, icon : 'devices' }; break; case 'performances' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__performances', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Site wide page speed optimizations'], icon : 'network_check' }; break; case 'recaptcha' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__recaptcha', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Protect your contact forms with Google reCAPTCHA'], icon : 'security' }; break; case 'global_revisions' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__global_revisions', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Revision history of global sections'], icon : 'history' }; break; case 'global_imp_exp' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__global_imp_exp', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Export / Import global sections'], icon : 'import_export' }; break; case 'global_reset' : registrationParams[ opt_name ] = { settingControlId : _id_ + '__global_reset', module_type : mod_type, controlLabel : sektionsLocalizedData.i18n['Remove the sections displayed in global locations'], icon : 'delete' }; break; case 'beta_features' : // may 2021 not rendered anymore // registrationParams[ opt_name ] = { // settingControlId : _id_ + '__beta_features', // module_type : mod_type, // controlLabel : sektionsLocalizedData.i18n['Beta features'], // icon : 'widgets' // }; break; default : api.errare('::generateUIforGlobalOptions => an option group could not be registered => ' + mod_type, opt_name ); break; }//switch });//_.each // Let assign the global options to a var var globalOptionDBValues = sektionsLocalizedData.globalOptionDBValues; _do_register_ = function() { _.each( registrationParams, function( optionData, optionType ){ if ( 'site_templates' === optionType ) { var _doThingsAfterRefresh = function() { // setTimeout( function() { // api.control( optionData.settingControlId ).focus(); // }, 300 ); api.trigger('nimble-update-topbar-skope-status'); api.previewer.trigger('sek-notify', { type : 'info', duration : 20000, message : [ '', //'' + sektionsLocalizedData.i18n['Template saved'] + '', sektionsLocalizedData.i18n['Refreshed to home page : site templates must be set when previewing home'], '' ].join('') }); api.previewer.unbind( 'czr-new-skopes-synced', _doThingsAfterRefresh ); setTimeout( function() { // This property is used to avoid the automatic focus on content picker when forcing preview on home while modifying site templates api._nimbleRefreshingPreviewHomeWhenSettingSiteTemplate = false; }, 1000); }; } if ( ! api.has( optionData.settingControlId ) ) { var doUpdate = function( to, from, args ) { try { self.updateAPISettingAndExecutePreviewActions({ isGlobalOptions : true,//<= indicates that we won't update the local skope setting id defaultPreviewAction : 'refresh_preview', uiParams : params, options_type : optionType, settingParams : { to : to, from : from, args : args } }); } catch( er ) { api.errare( '::generateUIforGlobalOptions => Error in updateAPISettingAndExecutePreviewActions', er ); } }; // Schedule the binding to synchronize the options with the main collection setting // Note 1 : unlike control or sections, the setting are not getting cleaned up on each ui generation. // They need to be kept in order to keep track of the changes in the customizer. // => that's why we check if ! api.has( ... ) api( optionData.settingControlId, function( _setting_ ) { // SITE TEMPLATE STUFFS // Added March 2021 for #478 // Force preview to home when modifying the site templates if ( 'site_templates' === optionType ) { _setting_.bind( function( to ) { // This property is used to avoid the automatic focus on content picker when forcing preview on home while modifying site templates api._nimbleRefreshingPreviewHomeWhenSettingSiteTemplate = true;//<= set to false in _doThingsAfterRefresh api.previewer.bind( 'czr-new-skopes-synced', _doThingsAfterRefresh ); api.previewer.previewUrl( api.settings.url.home ); api.trigger('nimble-update-topbar-skope-status'); }); } _setting_.bind( _.debounce( doUpdate, self.SETTING_UPDATE_BUFFER ) );//_setting_.bind( _.debounce( function( to, from, args ) {} });//api( Id, function( _setting_ ) {}) // Let's add the starting values if provided when registrating the module var startingModuleValue = self.getModuleStartingValue( optionData.module_type ), initialModuleValues = ( _.isObject( globalOptionDBValues ) && ! _.isEmpty( globalOptionDBValues[ optionType ] ) ) ? globalOptionDBValues[ optionType ] : {}; if ( 'no_starting_value' !== startingModuleValue && _.isObject( startingModuleValue ) ) { // make sure the starting values are deeped clone now, before being extended var clonedStartingModuleValue = $.extend( true, {}, startingModuleValue ); initialModuleValues = $.extend( clonedStartingModuleValue, initialModuleValues ); } api.CZR_Helpers.register( { origin : 'nimble', level : params.level, what : 'setting', id : optionData.settingControlId, dirty : false, value : initialModuleValues, transport : 'postMessage',//'refresh',//// , type : '_nimble_ui_'//will be dynamically registered but not saved in db as option// columnData.settingType }); } api.CZR_Helpers.register( { origin : 'nimble', level : params.level, what : 'control', id : optionData.settingControlId, label : optionData.controlLabel, type : 'czr_module',//sekData.controlType, module_type : optionData.module_type, section : self.SECTION_ID_FOR_GLOBAL_OPTIONS,//registered in ::initialize() priority : 20, settings : { default : optionData.settingControlId }, track : false//don't register in the self.registered() => this will prevent this container to be removed when cleaning the registered }).done( function() { // if ( true === optionData.expandAndFocusOnInit ) { // api.control( optionData.settingControlId ).focus({ // completeCallback : function() {} // }); // } // Implement the animated arrow markup, and the initial state of the module visibility api.control( optionData.settingControlId, function( _control_ ) { // Hide the item wrapper // @see css _control_.container.attr('data-sek-expanded', "false" ); var $title = _control_.container.find('label > .customize-control-title'), _titleContent = $title.html(); // We wrap the original text content in this span.sek-ctrl-accordion-title in order to style it (underlined) independently ( without styling the icons next to it ) $title.html( [ '', _titleContent, //'site_templates' === optionType ? ' New!' : '', '' ].join('') ); // if this level has an icon, let's prepend it to the title if ( ! _.isUndefined( optionData.icon ) ) { $title.addClass('sek-flex-vertical-center').prepend( optionData.icon ); } // prepend the animated arrow $title.prepend(''); // setup the initial state + initial click _control_.container.attr('data-sek-expanded', "false" ); if ( true === optionData.expandAndFocusOnInit && "false" == _control_.container.attr('data-sek-expanded' ) ) { $title.trigger('click'); } if ( 'site_templates' === optionType ) { _control_.container.one('click', '.customize-control-title', function() { // This property is used to avoid the automatic focus on content picker when forcing preview on home while modifying site templates api._nimbleRefreshingPreviewHomeWhenSettingSiteTemplate = true;//<= set to false in _doThingsAfterRefresh api.previewer.bind( 'czr-new-skopes-synced', _doThingsAfterRefresh ); api.previewer.previewUrl( api.settings.url.home ); api.trigger('nimble-update-topbar-skope-status'); }); } }); }); });//_.each(); };//do register // The parent section has already been added in ::initialize() _do_register_(); return dfd; } });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData, serverControlParams var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { // user action => this utility must be used to set the main setting value // params = { // action : 'sek-add-section', 'sek-add-column', 'sek-add-module',... // in_sektion // in_column // } updateAPISetting : function( params ) { var self = this; // Are we in global location ? // Add the global information to the params // => is used to determine the skope id when resolving the promise in reactToPreviewMsg params = params || {}; params.is_global_location = 'global' === params.scope || self.isGlobalLocation( params ); var _collectionSettingId_ = params.is_global_location ? self.getGlobalSectionsSettingId() : self.localSectionsSettingId(); var currentSetValue = api( _collectionSettingId_ )(); // The following property is populated on each api setting update // some properties are modified during the sub-callbacks of _updateSektionSettingInstanceWithAction self.updAPISetParams = { params : params, promise : $.Deferred(), newSetValue : _.isObject( currentSetValue ) ? $.extend( true, {}, currentSetValue ) : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ), cloneId : '', // the cloneId is only needed in the duplication scenarii sectionInjectPromise : '_not_injection_scenario_'//this property is turned into a $.Deferred() object in a scenario of section injection }; // callback ran in api( _collectionSettingId_, function( sektionSetInstance ) {}) var _updateSektionSettingInstanceWithAction = function() { // api( _collectionSettingId_)() = { // collection : [ // 'loop_start' : { level : location, collection : [ 'sek124' : { collection : [], level : section, options : {} }], options : {}}, // 'loop_end' : { level : location, collection : [], options : {}} // ... // ], // options : {} // // } // make sure we have a collection array to populate self.updAPISetParams.newSetValue.collection = _.isArray( self.updAPISetParams.newSetValue.collection ) ? self.updAPISetParams.newSetValue.collection : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ).collection; switch( params.action ) { //------------------------------------------------------------------------------------------------- //-- LOCATION //------------------------------------------------------------------------------------------------- // December 2020 => this action is triggered in ::initialize self.activeLocations.bind() // when injecting template from the gallery it may happen that the collection of location in the local setting value is not synchronized anymore with the actual active locations on the page // update : December 24th => deactivated because of https://github.com/presscustomizr/nimble-builder/issues/770 case 'sek-maybe-add-missing-locations' : var activeLocations = self.activeLocations(), settingLocations = [], locInSetting, missingLoc, newSettingCollection = [], currentSettingCollection = $.extend( true, [], self.updAPISetParams.newSetValue.collection ); //console.log('SOO current Setting Collection', currentSettingCollection, activeLocations ); // loop on the active locations of the current page. // if one is missing in the setting value, let's add it. _.each( activeLocations, function( _loc_id ) { locInSetting = _.findWhere( self.updAPISetParams.newSetValue.collection, { id : _loc_id } ); if ( _.isUndefined( locInSetting ) ) { missingLoc = $.extend( true, {}, sektionsLocalizedData.defaultLocationModel ); missingLoc.id = _loc_id; api.infoLog('=> need to add missing location to api setting value', missingLoc ); self.updAPISetParams.newSetValue.collection.push(missingLoc); } }); break; //------------------------------------------------------------------------------------------------- //-- SEKTION //------------------------------------------------------------------------------------------------- case 'sek-add-section' : self._updAPISet_sek_add_sektion(); break; case 'sek-duplicate-section' : self._updAPISet_sek_duplicate_sektion(); break; case 'sek-remove-section' : self._updAPISet_sek_remove_sektion(); break; case 'sek-move-section' : self._updAPISet_sek_move_sektion(); break; // Fired on click on up / down arrows in the section ui menu // This handles the nested sections case case 'sek-move-section-up-down' : self._updAPISet_sek_move_sektion_up_down(); break; //------------------------------------------------------------------------------------------------- //-- COLUMN //------------------------------------------------------------------------------------------------- case 'sek-add-column' : self._updAPISet_sek_add_column(); break; case 'sek-remove-column' : self._updAPISet_sek_remove_column(); break; case 'sek-duplicate-column' : self._updAPISet_sek_duplicate_column(); break; // Note : the css rules are generated in Sek_Dyn_CSS_Builder::sek_add_rules_for_column_width case 'sek-resize-columns' : self._updAPISet_sek_resize_column(); break; case 'sek-move-column' : self._updAPISet_sek_move_column(); break; //------------------------------------------------------------------------------------------------- //-- MODULE //------------------------------------------------------------------------------------------------- case 'sek-add-module' : self._updAPISet_sek_add_module(); break; case 'sek-duplicate-module' : self._updAPISet_sek_duplicate_module(); break; case 'sek-remove-module' : self._updAPISet_sek_remove_module(); break; case 'sek-move-module' : self._updAPISet_sek_move_module(); break; case 'sek-set-module-value' : self._updAPISet_sek_set_module_value(); break; //------------------------------------------------------------------------------------------------- //-- CONTENT IN NEW SEKTION //------------------------------------------------------------------------------------------------- case 'sek-add-content-in-new-sektion' : self._updAPISet_sek_add_content_in_new_sektion(); break; //------------------------------------------------------------------------------------------------- //-- CONTENT IN NEW NESTED SEKTION //------------------------------------------------------------------------------------------------- case 'sek-add-preset-section-in-new-nested-sektion' : self._updAPISet_sek_add_preset_sektion_in_new_nested_sektion(); break; //------------------------------------------------------------------------------------------------- //-- FILE IMPORT //------------------------------------------------------------------------------------------------- case 'sek-import-from-file' : self._updAPISet_sek_import_from_file(); break; //------------------------------------------------------------------------------------------------- //-- INJECT TEMPLATE FROM GALLERY //------------------------------------------------------------------------------------------------- case 'sek-inject-tmpl-from-gallery' : self._updAPISet_sek_inject_tmpl_from_gallery(); break; //------------------------------------------------------------------------------------------------- //-- LEVEL OPTIONS //------------------------------------------------------------------------------------------------- case 'sek-generate-level-options-ui' : var _candidate_ = self.getLevelModel( params.id, self.updAPISetParams.newSetValue.collection ), _valueCandidate = {}; if ( 'no_match'=== _candidate_ ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent sektion matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent sektion matched'); break; } // start from a deep cloned object // important => fixes https://github.com/presscustomizr/nimble-builder/issues/455 var _new_options_values = $.extend( true, {}, _candidate_.options || {} ); // consider only the non empty settings for db // booleans should bypass this check _.each( params.value || {}, 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_; }); if ( _.isEmpty( params.options_type ) ) { api.errare( 'updateAPISetting => ' + params.action + ' => missing options_type'); } _new_options_values[ params.options_type ] = _valueCandidate; _candidate_.options = _new_options_values; break; //------------------------------------------------------------------------------------------------- //-- LOCAL SKOPE OPTIONS //------------------------------------------------------------------------------------------------- // Note : this is saved in "local_options" case 'sek-generate-local-skope-options-ui' : _valueCandidate = {}; var _currentOptions = $.extend( true, {}, _.isObject( self.updAPISetParams.newSetValue.local_options ) ? self.updAPISetParams.newSetValue.local_options : {} ); // consider only the non empty settings for db // booleans should bypass this check _.each( params.value || {}, 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_; }); if ( _.isEmpty( params.options_type ) || ! _.isString( params.options_type ) ) { api.errare( 'updateAPISetting => ' + params.action + ' => missing options_type'); } else { var newOptionsValues = {}; newOptionsValues[ params.options_type ] = _valueCandidate; self.updAPISetParams.newSetValue.local_options = $.extend( _currentOptions, newOptionsValues ); } break; //------------------------------------------------------------------------------------------------- //-- POPULATE GOOGLE FONTS //------------------------------------------------------------------------------------------------- //@params { // action : 'sek-update-fonts', // font_family : newFontFamily, // } case 'sek-update-fonts' : // Get the gfonts from the level options and modules values var currentGfonts = self.sniffGFonts( { is_global_location : ( params && true === params.is_global_location ) } ); // add it only if gfont if ( ! _.isEmpty( params.font_family ) && _.isString( params.font_family ) ) { if ( params.font_family.indexOf('gfont') > -1 && ! _.contains( currentGfonts, params.font_family ) ) { currentGfonts.push( params.font_family ); } } // 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 self.updAPISetParams.newSetValue.fonts = currentGfonts; break; //------------------------------------------------------------------------------------------------- //-- RESTORE A REVISION //------------------------------------------------------------------------------------------------- case 'sek-restore-revision' : //api.infoLog( 'sek-restore-revision', params ); self.updAPISetParams.newSetValue = params.revision_value; break; //------------------------------------------------------------------------------------------------- //-- RESET COLLECTION, LOCAL OR GLOBAL //------------------------------------------------------------------------------------------------- case 'sek-reset-collection' : //api.infoLog( 'sek-import-from-file', params ); var _localOptions; if ( 'local' === params.scope ) { _localOptions = $.extend( true, {}, _.isObject( self.updAPISetParams.newSetValue.local_options ) ? self.updAPISetParams.newSetValue.local_options : {} ); } try { self.updAPISetParams.newSetValue = api.czr_sektions.resetCollectionSetting( params.scope, _localOptions ); } catch( er ) { api.errare( 'sek-reset-collection => error when firing resetCollectionSetting()', er ); } break; }// switch // if we did not already rejected the request, let's check if the setting object has actually been modified // at this point it should have been. if ( 'pending' == self.updAPISetParams.promise.state() ) { var mayBeUpdateSektionsSetting = function() { // When a sektion setting is changed, "from" and "to" are passed to the .settingParams property // settingParams : { // to : to, // from : from, // args : args // } // @see for example ::generateUIforFrontModules or ::generateUIforLevelOptions var isSettingValueChangeCase = params.settingParams && params.settingParams.from && params.settingParams.to; // in a setting value change case, the from and to must be different // implemented when fixing https://github.com/presscustomizr/nimble-builder/issues/455 if ( isSettingValueChangeCase && _.isEqual( params.settingParams.from, params.settingParams.to ) ) { self.updAPISetParams.promise.reject( 'updateAPISetting => main sektion setting change => the new setting value is unchanged when firing action : ' + params.action ); } else if ( ! isSettingValueChangeCase && _.isEqual( currentSetValue, self.updAPISetParams.newSetValue ) ) { self.updAPISetParams.promise.reject( 'updateAPISetting => the new setting value is unchanged when firing action : ' + params.action ); } else { // method ::validateSettingValue() returns null if there is at least one validation error var _settingValidationResult = self.validateSettingValue( self.updAPISetParams.newSetValue, params.is_global_location ? 'global' : 'local' ); if ( null !== _settingValidationResult && !_.isUndefined(_settingValidationResult) ) { if ( !params.is_global_location ) { // INHERITANCE // solves the problem of preventing group template inheritance after a local reset // on ::resetCollectionSetting(), the setting val is being modified to add this property local_reset.inherit_group_scope var _is_inheritance_enabled_in_local_options = true, newSetVal = self.updAPISetParams.newSetValue; if ( newSetVal.local_options && newSetVal.local_options.local_reset && !_.isUndefined( newSetVal.local_options.local_reset.inherit_group_scope ) ) { _is_inheritance_enabled_in_local_options = newSetVal.local_options.local_reset.inherit_group_scope; } // Added March 2021 for #478 // 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 ( see js control:: resetCollectionSetting ) // Note : If this property is set to true => NB removes the local skope post in Nimble_Collection_Setting::update() self.updAPISetParams.newSetValue.__inherits_group_skope_tmpl_when_exists__ = 'sek-reset-collection' === params.action && _is_inheritance_enabled_in_local_options; } api( _collectionSettingId_ )( self.updAPISetParams.newSetValue, params ); // April 2021 : for site templates if ( !params.is_global_location ) { api.trigger('nimble-update-topbar-skope-status'); } // Add the cloneId to the params when we resolve // the cloneId is only needed in the duplication scenarii params.cloneId = self.updAPISetParams.cloneId; self.updAPISetParams.promise.resolve( params ); } else { self.updAPISetParams.promise.reject( 'Validation problem for action ' + params.action ); } //api.infoLog('COLLECTION SETTING UPDATED => ', _collectionSettingId_, api( _collectionSettingId_ )() ); } };//mayBeUpdateSektionsSetting() // For all scenarios except section injection, we can update the sektion setting now // otherwise we need to wait for the injection to be processed asynchronously // CRITICAL => self.updAPISetParams.promise has to be resolved / rejected // otherwise this can lead to scenarios where a change is not taken into account in ::updateAPISettingAndExecutePreviewActions // like in https://github.com/presscustomizr/nimble-builder/issues/373 if ( '_not_injection_scenario_' === self.updAPISetParams.sectionInjectPromise ) { mayBeUpdateSektionsSetting(); // At this point the self.updAPISetParams.promise obj can't be in a 'pending' state if ( 'pending' === self.updAPISetParams.promise.state() ) { api.errare( '::updateAPISetting => The self.updAPISetParams.promise has not been resolved properly.'); } } else { self.updAPISetParams.sectionInjectPromise .done( function() { mayBeUpdateSektionsSetting(); // At this point the self.updAPISetParams.promise obj can't be in a 'pending' state if ( 'pending' === self.updAPISetParams.promise.state() ) { api.errare( '::updateAPISetting => The self.updAPISetParams.promise has not been resolved properly.'); } }) .fail( function( _er_ ) { api.errare( 'updateAPISetting => self.updAPISetParams.sectionInjectPromise failed', _er_ ); }); } } };//_updateSektionSettingInstanceWithAction() // Update the sektion collection api( _collectionSettingId_, function( sektionSetInstance ) { _updateSektionSettingInstanceWithAction(); }); return self.updAPISetParams.promise.promise(); },//updateAPISetting // used on : // - add column // - remove column // - duplicate column // - move column // added in June 2019 for https://github.com/presscustomizr/nimble-builder/issues/279 resetColumnsWidthInSection : function( sektionCandidate ) { // RESET ALL COLUMNS WIDTH _.each( sektionCandidate.collection, function( colModel ) { if ( colModel.options && colModel.options.width && colModel.options.width['custom-width'] ) { colModel.options.width = _.omit( colModel.options.width, 'custom-width' ); } colModel.width = '';// For backward compat since June 2019 }); }, // @return a promise() // caches the api sections in api.nimble_ApiSections when api.section( '__content_picker__') is registered // caches the user saved sections on the first drag and drop of a user-saved section _getApiSingleSectionData : function( presetSectionId ) { var dfd = $.Deferred(), _ajaxRequest_; // If already cached, resolve now if ( ! _.isEmpty( api.nimble_ApiSections[presetSectionId] ) ) { dfd.resolve( api.nimble_ApiSections[presetSectionId] ); } else { if ( ! _.isUndefined( api.nimble_fetchingApiSection ) && 'pending' == api.nimble_fetchingApiSection.state() ) { _ajaxRequest_ = api.nimble_fetchingApiSection; } else { _ajaxRequest_ = wp.ajax.post( 'sek_get_single_api_section_data', { nonce: api.settings.nonce.save, api_section_id : presetSectionId }); api.nimble_fetchingApiSection = _ajaxRequest_; } _ajaxRequest_.done( function( _section_data_ ) { //api.nimble_ApiSections = JSON.parse( _section_data_ ); api.nimble_ApiSections[presetSectionId] = _section_data_; dfd.resolve( _section_data_ ); }).fail( function( _r_ ) { api.errorLog( 'ajax sek_get_single_api_section_data => error', _r_ ); var _msg = 'Error when fetching the section'; if ( _.isString( _r_ ) && !_.isEmpty( _r_ ) ) { _msg = _r_; } api.previewer.trigger('sek-notify', { type : 'error', duration : 60000, is_pro_notif : true, notif_id : 'pro_section_error', message : [ '', ''+ _msg + '', '' ].join('') }); }); } return dfd.promise(); }, // First run : fetches the collection from the server // Next runs : uses the cached collection // // @return a JSON parsed string, // + guid() ids for each levels // ready for insertion // // @sectionParams : { // is_user_section : bool, //<= is this section a "saved" section ? // presetSectionId : params.content_id, // } getPresetSectionCollectionData : function( sectionParams ) { var self = this, __dfd__ = $.Deferred(); if ( sectionParams.is_user_section ) { wp.ajax.post( 'sek_get_user_section_json', { nonce: api.settings.nonce.save, section_post_name: sectionParams.presetSectionId //skope_id: api.czr_skopeBase.getSkopeProperty( 'skope_id' ) }) .done( function( userSection ) { // User section looks like // { // data : {} // metas : {} // section_post_name : '' // } if ( ! _.isObject( userSection ) || _.isEmpty( userSection ) || _.isUndefined( userSection.data ) ) { api.errare( 'getPresetSectionCollectionData => preset section type not found or empty : ' + sectionParams.presetSectionId, userSection ); throw new Error( 'getPresetSectionCollectionData => preset section type not found or empty'); } var userSectionCandidate = $.extend( {}, true, userSection.data ); // ID's // the level's id have to be generated // 1) root level userSectionCandidate.id = sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(); // 2) children levels userSectionCandidate.collection = self.setPresetOrUserSectionIds( userSectionCandidate.collection ); // NIMBLE VERSION // set the section version userSectionCandidate.ver_ini = sektionsLocalizedData.nimbleVersion; // the other level's version have to be added userSectionCandidate.collection = self.setPresetSectionVersion( userSectionCandidate.collection ); // OK. time to resolve self.updAPISetParams.sectionInjectPromise.promise() __dfd__.resolve( userSectionCandidate ); }) .fail( function( er ) { __dfd__.reject( er ); }); } else { api.nimble_ApiSections = api.nimble_ApiSections || {}; var _doResolveDfdWithData = function( _section_data_ ) { //api.infoLog( 'API SECTION fetched', sectionParams.presetSectionId, api.nimble_ApiSections ); if ( _.isEmpty( _section_data_ ) || !_.isObject(_section_data_) ) { throw new Error( 'getPresetSectionCollectionData => Invalid collection'); } // if ( _.isEmpty( allPresets[ sectionParams.presetSectionId ] ) ) { // throw new Error( 'getPresetSectionCollectionData => the preset section : "' + sectionParams.presetSectionId + '" has not been found in the collection'); // } var presetCandidate = $.extend( true, {}, _section_data_ ); // Ensure we have a string that's JSON.parse-able // if ( typeof presetCandidate !== 'string' || presetCandidate[0] !== '{' ) { // throw new Error( 'getPresetSectionCollectionData => ' + sectionParams.presetSectionId + ' is not JSON.parse-able'); // } // presetCandidate = JSON.parse( presetCandidate ); // ID's // the level's id have to be generated presetCandidate.collection = self.setPresetOrUserSectionIds( presetCandidate.collection ); // NIMBLE VERSION // set the section version presetCandidate.ver_ini = sektionsLocalizedData.nimbleVersion; // the other level's version have to be added presetCandidate.collection = self.setPresetSectionVersion( presetCandidate.collection ); __dfd__.resolve( presetCandidate ); }; var _collection; switch( sectionParams.presetSectionId ) { case 'two_columns' : _collection = JSON.parse('{"collection":[{"id":"","level":"column","collection":[]},{"id":"","level":"column","collection":[]}]}'); _doResolveDfdWithData(_collection); break; case 'three_columns' : _collection = JSON.parse('{"collection":[{"id":"","level":"column","collection":[]},{"id":"","level":"column","collection":[]},{"id":"","level":"column","collection":[]}]}'); _doResolveDfdWithData(_collection); break; case 'four_columns' : _collection = JSON.parse('{"collection":[{"id":"","level":"column","collection":[]},{"id":"","level":"column","collection":[]},{"id":"","level":"column","collection":[]},{"id":"","level":"column","collection":[]}]}'); _doResolveDfdWithData(_collection); break; default : self._getApiSingleSectionData( sectionParams.presetSectionId ) .fail( function( er ) { __dfd__.reject( er ); }) .done( _doResolveDfdWithData );//_getApiSingleSectionData.done() break; }// Switch() } return __dfd__.promise(); }, // SECTION HELPERS // Replaces __rep__me__ by an id looking like __nimble__1eda909f0cfe setPresetOrUserSectionIds : function( collection ) { var self = this; // Only collection set as arrays hold columns or modules if ( _.isArray( collection ) ) { _.each( collection, function( levelData ) { levelData.id = sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(); if ( _.isArray( levelData.collection ) ) { self.setPresetOrUserSectionIds( levelData.collection ); } }); } return collection; }, setPresetSectionVersion : function( collection ) { var self = this; _.each( collection, function( levelData ) { levelData.ver_ini = sektionsLocalizedData.nimbleVersion; if ( _.isArray( levelData.collection ) ) { self.setPresetSectionVersion( levelData.collection ); } }); return collection; }, // Walk the column collection of a preset section, and replace '__img_url__*' pattern by image ids that we get from ajax calls // Is designed to handle multiple ajax calls in parallel if the preset_section includes several images // @return a promise() preparePresetSectionForInjection : function( columnCollection ) { var self = this, deferreds = {}, preparedSection = {}, _dfd_ = $.Deferred(); // items id of multi-items module must always be unique // this recursive method sniff and does the job self.maybeGenerateNewItemIdsForCrudModules( columnCollection ); // walk the column collection and populates the deferreds object recursively var _sniffImg = function( data ) { _.each( data, function( val, key ) { if ( _.isObject( val ) || _.isArray( val ) ) { _sniffImg( val ); } else if ( _.isString( val ) && -1 != val.indexOf( '__img_url__' ) ) { // scenario when a section uses an image more than once. // => we don't need to fire a new ajax request for an image already sniffed if ( ! _.has( deferreds, val ) ) { deferreds[ val ] = self.importAttachment( val.replace( '__img_url__', '' ) ); } } }); return deferreds; }; // walk the column collection and populates the deferreds object recursively // imdList is formed this way : // __img_url__/assets/img/1.jpg : {id: 2547, url: "http://customizr-dev.test/wp-content/uploads/2018/09/nimble_asset_1.jpg"} // __img_url__/assets/img/2.jpg : {id: 2548, url: "http://customizr-dev.test/wp-content/uploads/2018/09/nimble_asset_2.jpg"} // __img_url__/assets/img/3.jpg : {id: 2549, url: "http://customizr-dev.test/wp-content/uploads/2018/09/nimble_asset_3.jpg"} var _replaceImgPlaceholderById = function( data, imgList) { _.each( data, function( val, key ) { if ( _.isObject( val ) || _.isArray( val ) ) { _replaceImgPlaceholderById( val, imgList ); } else if ( _.isString( val ) && -1 != val.indexOf( '__img_url__' ) && _.has( imgList, val ) && _.isObject( imgList[ val ] ) ) { data[ key ] = imgList[ val ].id; } }); return columnCollection; }; self.whenAllPromisesInParallel( _sniffImg( columnCollection ) ) .done( function( imgList ) { var imgReadySection = _replaceImgPlaceholderById( columnCollection, imgList ); _dfd_.resolve( imgReadySection ); }) .fail( function( _er_ ){ _dfd_.reject( _er_ ); }); return _dfd_.promise(); } });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData, serverControlParams //self.updAPISetParams = { // params : params, // promise : $.Deferred(), // newSetValue : _.isObject( _currentSetValue ) ? $.extend( true, {}, _currentSetValue ) : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ), // cloneId : '', // sectionInjectPromise // }; var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { //------------------------------------------------------------------------------------------------- //-- ADD SECTION //------------------------------------------------------------------------------------------------- _updAPISet_sek_add_sektion : function() { var self = this, params, columnCandidate, parentSektionCandidate, locationCandidate; params = self.updAPISetParams.params; //console.log('ADD SECTION ?', params, self.updAPISetParams ); // an id must be provided if ( _.isEmpty( params.id ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing id' ); } if ( _.isEmpty( params.location ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing location' ); } // Is this a nested sektion ? if ( true === params.is_nested ) { columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); // can we add this nested sektion ? // if the parent sektion of the column has is_nested = true, then we can't parentSektionCandidate = self.getLevelModel( params.in_sektion, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == parentSektionCandidate ) { self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no grand parent sektion found'); } else if ( true === parentSektionCandidate.is_nested ) { self.updAPISetParams.promise.reject(''); api.previewer.trigger('sek-notify', { type : 'info', duration : 30000, message : [ '', '' + sektionsLocalizedData.i18n[ "You've reached the maximum number of allowed nested sections." ] + '', '' ].join('') }); } else if ( 'no_match' == columnCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent column matched'); } else { columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; columnCandidate.collection.push({ id : params.id, level : 'section', collection : [{ id : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(), level : 'column', collection : [], ver_ini : sektionsLocalizedData.nimbleVersion }], is_nested : true, ver_ini : sektionsLocalizedData.nimbleVersion }); } } else { locationCandidate = self.getLevelModel( params.location, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == locationCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no location matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no location matched'); } else { var position = 0; locationCandidate.collection = _.isArray( locationCandidate.collection ) ? locationCandidate.collection : []; _.each( locationCandidate.collection, function( secModel, index ) { if ( params.before_section === secModel.id ) { position = index; } if ( params.after_section === secModel.id ) { position = index + 1; } }); // @see reactToCollectionSettingIdChange locationCandidate.collection = _.isArray( locationCandidate.collection ) ? locationCandidate.collection : []; // insert the section in the collection at the right place locationCandidate.collection.splice( position, 0, { id : params.id, level : 'section', collection : [{ id : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(), level : 'column', collection : [], ver_ini : sektionsLocalizedData.nimbleVersion }], ver_ini : sektionsLocalizedData.nimbleVersion }); } } },//_updAPISet_sek_add_sektion //------------------------------------------------------------------------------------------------- //-- DUPLICATE SECTION //------------------------------------------------------------------------------------------------- _updAPISet_sek_duplicate_sektion : function() { var self = this, params, columnCandidate, locationCandidate, deepClonedSektion; params = self.updAPISetParams.params; // an id must be provided if ( _.isEmpty( params.id ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing id' ); } if ( _.isEmpty( params.location ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing location' ); } try { deepClonedSektion = self.cloneLevel( params.id ); } catch( er ) { api.errare( 'updateAPISetting => ' + params.action, er ); return; } // items id of multi-items module must always be unique // this recursive method sniff and does the job self.maybeGenerateNewItemIdsForCrudModules( deepClonedSektion ); var _position_ = self.getLevelPositionInCollection( params.id, self.updAPISetParams.newSetValue.collection ); // Is this a nested sektion ? if ( true === params.is_nested ) { columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == columnCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent column matched'); return; } columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; columnCandidate.collection.splice( parseInt( _position_ + 1, 10 ), 0, deepClonedSektion ); } else { locationCandidate = self.getLevelModel( params.location, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == locationCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no location matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no location matched'); return; } locationCandidate.collection = _.isArray( locationCandidate.collection ) ? locationCandidate.collection : []; // @see reactToCollectionSettingIdChange locationCandidate.collection.splice( parseInt( _position_ + 1, 10 ), 0, deepClonedSektion ); } self.updAPISetParams.cloneId = deepClonedSektion.id;//will be passed in resolve() },//_updAPISet_sek_duplicate_sektion //------------------------------------------------------------------------------------------------- //-- REMOVE SECTION //------------------------------------------------------------------------------------------------- // in the case of a nested sektion, we have to remove it from a column // otherwise from the root sektion collection _updAPISet_sek_remove_sektion : function() { var self = this, params, columnCandidate, locationCandidate; params = self.updAPISetParams.params; //api.infoLog('PARAMS IN sek-remove-sektion', params ); if ( true === params.is_nested ) { columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' != columnCandidate ) { columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; columnCandidate.collection = _.filter( columnCandidate.collection, function( col ) { return col.id != params.id; }); } else { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); } } else { locationCandidate = self.getLevelModel( params.location, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == locationCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no location matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no location matched'); return; } locationCandidate.collection = _.filter( locationCandidate.collection, function( sek ) { return sek.id != params.id; }); } }, //------------------------------------------------------------------------------------------------- //-- MOVE SECTION (DRAG IN THE PAGE INSIDE A LOCATION, OR CROSS LOCATIONS) //------------------------------------------------------------------------------------------------- _updAPISet_sek_move_sektion : function() { var self = this, params, originalCollection, reorderedCollection, locationCandidate; params = self.updAPISetParams.params; //api.infoLog('PARAMS in sek-move-section', params ); var toLocationCandidate = self.getLevelModel( params.to_location, self.updAPISetParams.newSetValue.collection ), movedSektionCandidate, copyOfMovedSektionCandidate; if ( _.isEmpty( toLocationCandidate ) || 'no_match' == toLocationCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing target location' ); } // MOVED CROSS LOCATIONS // - make a copy of the moved sektion // - remove the moved sektion from the source location if ( params.from_location != params.to_location ) { // Remove the moved sektion from the source location var fromLocationCandidate = self.getLevelModel( params.from_location, self.updAPISetParams.newSetValue.collection ); if ( _.isEmpty( fromLocationCandidate ) || 'no_match' == fromLocationCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing source location' ); } fromLocationCandidate.collection = _.isArray( fromLocationCandidate.collection ) ? fromLocationCandidate.collection : []; // Make a copy of the sektion candidate now, before removing it movedSektionCandidate = self.getLevelModel( params.id, fromLocationCandidate.collection ); copyOfMovedSektionCandidate = $.extend( true, {}, movedSektionCandidate ); // remove the sektion from its previous sektion fromLocationCandidate.collection = _.filter( fromLocationCandidate.collection, function( sektion ) { return sektion.id != params.id; }); } // UPDATE THE TARGET LOCATION toLocationCandidate.collection = _.isArray( toLocationCandidate.collection ) ? toLocationCandidate.collection : []; originalCollection = $.extend( true, [], toLocationCandidate.collection ); reorderedCollection = []; _.each( params.newOrder, function( _id_ ) { // in the case of a cross location movement, we need to add the moved sektion to the target location if ( params.from_location != params.to_location && _id_ == copyOfMovedSektionCandidate.id ) { reorderedCollection.push( copyOfMovedSektionCandidate ); } else { sektionCandidate = self.getLevelModel( _id_, originalCollection ); if ( _.isEmpty( sektionCandidate ) || 'no_match' == sektionCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing section candidate' ); } reorderedCollection.push( sektionCandidate ); } }); toLocationCandidate.collection = reorderedCollection; },//_updAPISet_sek_move_sektion //------------------------------------------------------------------------------------------------- //-- MOVE SECTION UP DOWN //------------------------------------------------------------------------------------------------- _updAPISet_sek_move_sektion_up_down : function() { var self = this, params, parentCandidate, originalCollection, reorderedCollection, locationCandidate; params = self.updAPISetParams.params; parentCandidate = self.getLevelModel( params.is_nested ? params.in_column : params.location , self.updAPISetParams.newSetValue.collection ); if ( _.isEmpty( parentCandidate ) || 'no_match' == parentCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing target location' ); } parentCandidate.collection = _.isArray( parentCandidate.collection ) ? parentCandidate.collection : []; originalCollection = $.extend( true, [], parentCandidate.collection ); reorderedCollection = $.extend( true, [], parentCandidate.collection ); var _indexInOriginal = _.findIndex( originalCollection, function( _sec_ ) { return _sec_.id === params.id; }); // @see https://underscorejs.org/#findIndex if ( -1 === _indexInOriginal ) { throw new Error( 'updateAPISetting => ' + params.action + ' => invalid index' ); } // Swap up <=> down var direction = params.direction || 'up', isLastSectionInLocation = originalCollection.length === _indexInOriginal + 1, isFirstSectionInLocation = 0 === _indexInOriginal, _locInfo = self.activeLocationsInfo(), _currentLocInfo = ! _.isArray( _locInfo ) ? {} : _.findWhere( _locInfo, { id : params.location } ), isCurrentLocationGlobal = false, isCurrentLocationHeaderOrFooter = false; // Is current location global ? isCurrentLocationGlobal = _.isObject( _currentLocInfo ) && _currentLocInfo.is_global; // Is current location header footer ? isCurrentLocationHeaderOrFooter = _.isObject( _currentLocInfo ) && _currentLocInfo.is_header_footer; // When a section is last in location and there's another location below, let's move the section to this sibling location // This is possible when : // - when moved section is not nested // - only in locations that are 'local', not header or footer // Populate the eligible activeLocations in the page var activeLocationsInPage = []; // self.activeLocationsInfo() is set on ::initialized when send by the preview, and is structured the following way : // [ // { // id: "loop_start" // is_global: false // is_header_footer: false // }, // {..}, // . if ( _.isArray( _locInfo ) ) { _.each( self.activeLocationsInfo(), function( _loc_ ) { if ( ! _loc_.is_global && ! _loc_.is_header_footer ) { activeLocationsInPage.push( _loc_.id ); } }); } // Set the index of the current location var indexOfCurrentLocation = _.findIndex( activeLocationsInPage, function( _loc_id ) { return _loc_id === params.location; }); var isCurrentLocationEligibleForSectionMoveOutside = ! params.is_nested && ! isCurrentLocationGlobal && ! isCurrentLocationHeaderOrFooter, isFirstLocationInPage = 0 === indexOfCurrentLocation, isLastLocationInPage = activeLocationsInPage.length === indexOfCurrentLocation + 1, newLocationId, newLocationCandidate; if ( isCurrentLocationEligibleForSectionMoveOutside && isLastSectionInLocation && 'up' !== direction && ! isLastLocationInPage ) { newLocationId = activeLocationsInPage[ indexOfCurrentLocation + 1 ]; newLocationCandidate = self.getLevelModel( newLocationId , self.updAPISetParams.newSetValue.collection ); // Add the section in first position of the section below newLocationCandidate.collection.unshift( originalCollection[ _indexInOriginal ] ); // Removes the section in last position of the original section parentCandidate.collection.pop(); // the new_location param will be used in the 'complete' callback of 'sek-move-section-down' / 'sek-move-section-up' params.new_location = newLocationId; } else if ( isCurrentLocationEligibleForSectionMoveOutside && isFirstSectionInLocation && 'up' === direction && ! isFirstLocationInPage ) { newLocationId = activeLocationsInPage[ indexOfCurrentLocation - 1 ]; newLocationCandidate = self.getLevelModel( newLocationId , self.updAPISetParams.newSetValue.collection ); // Add the section in first position of the section below newLocationCandidate.collection.push( originalCollection[ _indexInOriginal ] ); // Removes the section in last position of the original section parentCandidate.collection.shift(); // the new_location param will be used in the 'complete' callback of 'sek-move-section-down' / 'sek-move-section-up' params.new_location = newLocationId; } else { // prevent absurd movements of a section // this should not happen because up / down arrows are not displayed when section is positionned top / bottom // but safer to add it if ( 'up' !== direction && originalCollection.length === _indexInOriginal + 1 ) { //throw new Error( 'updateAPISetting => ' + params.action + ' => bottom reached' ); api.previewer.trigger('sek-notify', { type : 'info', duration : 30000, message : [ '', '' + sektionsLocalizedData.i18n[ "The section cannot be moved lower." ] + '', '' ].join('') }); } else if ( 'up' === direction && 0 === _indexInOriginal ){ api.previewer.trigger('sek-notify', { type : 'info', duration : 30000, message : [ '', '' + sektionsLocalizedData.i18n[ "The section cannot be moved higher." ] + '', '' ].join('') }); } else { reorderedCollection[ _indexInOriginal ] = originalCollection[ 'up' === direction ? _indexInOriginal - 1 : _indexInOriginal + 1 ]; reorderedCollection[ 'up' === direction ? _indexInOriginal - 1 : _indexInOriginal + 1 ] = originalCollection[ _indexInOriginal ]; parentCandidate.collection = reorderedCollection; } } }//_updAPISet_sek_move_sektion_up_down });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData, serverControlParams //self.updAPISetParams = { // params : params, // promise : $.Deferred(), // newSetValue : _.isObject( _currentSetValue ) ? $.extend( true, {}, _currentSetValue ) : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ), // cloneId : '', // sectionInjectPromise // }; var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { // @params { // drop_target_element : $(this), // position : _position,// <= top or bottom // before_section : $(this).data('sek-before-section'), // after_section : $(this).data('sek-after-section'), // content_type : event.originalEvent.dataTransfer.getData( "sek-content-type" ), //<= module or preset_section // content_id : event.originalEvent.dataTransfer.getData( "sek-content-id" ) // } _updAPISet_sek_add_content_in_new_sektion : function() { var self = this, params, columnCandidate, locationCandidate; params = self.updAPISetParams.params; // get the position of the before or after section var positionIndex = 0, startingModuleValue; locationCandidate = self.getLevelModel( params.location, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == locationCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no location matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no location matched'); return; } locationCandidate.collection = _.isArray( locationCandidate.collection ) ? locationCandidate.collection : []; _.each( locationCandidate.collection, function( secModel, index ) { if ( params.before_section === secModel.id ) { positionIndex = index; } if ( params.after_section === secModel.id ) { positionIndex = index + 1; } }); switch( params.content_type) { // When a module is dropped in a section + column structure to be generated case 'module' : // Let's add the starting value if provided when registrating the module // Note : params.content_id is the module_type startingModuleValue = self.getModuleStartingValue( params.content_id ); // insert the section in the collection at the right place locationCandidate.collection.splice( positionIndex, 0, { id : params.id, level : 'section', collection : [ { id : sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(), level : 'column', collection : [ { id : params.droppedModuleId, level : 'module', module_type : params.content_id, value : 'no_starting_value' !== startingModuleValue ? startingModuleValue : null, ver_ini : sektionsLocalizedData.nimbleVersion } ], ver_ini : sektionsLocalizedData.nimbleVersion } ], ver_ini : sektionsLocalizedData.nimbleVersion }); break;//case 'module' : // When a preset section is dropped case 'preset_section' : // insert the section in the collection at the right place self.updAPISetParams.sectionInjectPromise = $.Deferred();//defined at the beginning of the method // we use a section index here to display the section in the same order as in the json // introduced when implementing multi-section pre-build section // @see https://github.com/presscustomizr/nimble-builder/issues/489 var _injectPresetSectionInLocationOrParentColumn = function( sectionReadyToInject, positionIndex ) { // If the preset_section is inserted in a an empty nested section, add it at the right place in the parent column of the nested section. // Otherwise, add the preset section at the right position in the parent location of the section. var insertedInANestedSektion = false; if ( ! _.isEmpty( params.sektion_to_replace ) ) { var sektionToReplace = self.getLevelModel( params.sektion_to_replace, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' === sektionToReplace ) { api.errare( 'updateAPISetting => ' + params.action + ' => no sektionToReplace matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no sektionToReplace matched'); } insertedInANestedSektion = true === sektionToReplace.is_nested; } // The following param "collection_of_preset_section_id" has been introduced when implementing support for multi-section pre-build sections // @see https://github.com/presscustomizr/nimble-builder/issues/489 // It is sent to the preview with ::reactToPreviewMsg, see bottom of the method. var injected_section_id = sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(); params.collection_of_preset_section_id = params.collection_of_preset_section_id || []; params.collection_of_preset_section_id.push( injected_section_id ); if ( ! insertedInANestedSektion ) { locationCandidate.collection.splice( positionIndex, 0, { id : injected_section_id,//params.id,//self.guid() level : 'section', collection : sectionReadyToInject.collection, options : sectionReadyToInject.options || {}, ver_ini : sektionsLocalizedData.nimbleVersion }); } else { columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' === columnCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent column matched'); } columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; // get the position of the before or after module _.each( columnCandidate.collection, function( moduleOrSectionModel, index ) { if ( params.before_section === moduleOrSectionModel.id ) { positionIndex = index; } if ( params.after_section === moduleOrSectionModel.id ) { positionIndex = index + 1; } }); columnCandidate.collection.splice( positionIndex, 0, { id : injected_section_id, is_nested : true, level : 'section', collection : sectionReadyToInject.collection, options : sectionReadyToInject.options || {}, ver_ini : sektionsLocalizedData.nimbleVersion }); } };//_injectPresetSectionInLocationOrParentColumn var _doWhenPresetSectionCollectionFetched = function( presetColumnOrSectionCollection ) { self.preparePresetSectionForInjection( presetColumnOrSectionCollection ) .fail( function( _er_ ){ self.updAPISetParams.promise.reject( 'updateAPISetting => error when preparePresetSectionForInjection => ' + params.action + ' => ' + _er_ ); // Used when updating the setting // @see end of this method self.updAPISetParams.sectionInjectPromise.reject( _er_ ); }) .done( function( maybeMultiSectionReadyToInject ) { var is_multi_section = 'section' === maybeMultiSectionReadyToInject.collection[0].level; // support for pre-built multi-section has been introduced in july 2019 // @see https://github.com/presscustomizr/nimble-builder/issues/489 if ( is_multi_section ) { // we use a section index here to display the section in the same order as in the json var sectionIndex = 0; _.each( maybeMultiSectionReadyToInject.collection, function( sectionReadyToInject ) { _injectPresetSectionInLocationOrParentColumn( sectionReadyToInject, positionIndex ); positionIndex++; }); } else { _injectPresetSectionInLocationOrParentColumn( maybeMultiSectionReadyToInject, positionIndex ); } // Used when updating the setting // @see end of this method self.updAPISetParams.sectionInjectPromise.resolve(); });// self.preparePresetSectionForInjection.done() };//_doWhenPresetSectionCollectionFetched() // Try to fetch the sections from the server // if sucessfull, resolve self.updAPISetParams.sectionInjectPromise.promise() self.getPresetSectionCollectionData({ is_user_section : params.is_user_section, presetSectionId : params.content_id }) .fail( function( _er_ ) { api.errare( 'updateAPISetting => ' + params.action + ' => Error with self.getPresetSectionCollectionData()', _er_ ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => Error with self.getPresetSectionCollectionData()'); }) .done( function( presetColumnOrSectionCollection ) { if ( ! _.isObject( presetColumnOrSectionCollection ) || _.isEmpty( presetColumnOrSectionCollection ) ) { api.errare( 'updateAPISetting => ' + params.action + ' => preset section type not found or empty : ' + params.content_id, presetColumnOrSectionCollection ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => preset section type not found or empty'); } // OK. time to resolve self.updAPISetParams.sectionInjectPromise.promise() _doWhenPresetSectionCollectionFetched( presetColumnOrSectionCollection ); });//self.getPresetSectionCollectionData().done() break;//case 'preset_section' : }//switch( params.content_type) }, // @params { // drop_target_element : $(this), // position : _position,// <= top or bottom // before_section : $(this).data('sek-before-section'), // after_section : $(this).data('sek-after-section'), // content_type : event.originalEvent.dataTransfer.getData( "sek-content-type" ), //<= module or preset_section // content_id : event.originalEvent.dataTransfer.getData( "sek-content-id" ) // } _updAPISet_sek_add_preset_sektion_in_new_nested_sektion : function() { var self = this, params, columnCandidate, parentSektionCandidate; params = self.updAPISetParams.params; columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' === columnCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent column matched'); return; } var positionIndexInColumn = 0; columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; // get the position of the before or after module or nested section _.each( columnCandidate.collection, function( moduleOrNestedSectionModel, index ) { if ( params.before_module_or_nested_section === moduleOrNestedSectionModel.id ) { positionIndexInColumn = index; } if ( params.after_module_or_nested_section === moduleOrNestedSectionModel.id ) { positionIndexInColumn = index + 1; } }); // can we add this nested sektion ? // if the parent sektion of the column has is_nested = true, then we can't parentSektionCandidate = self.getLevelModel( params.in_sektion, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == parentSektionCandidate ) { self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no grand parent sektion found'); return; } if ( true === parentSektionCandidate.is_nested ) { self.updAPISetParams.promise.reject(''); api.previewer.trigger('sek-notify', { type : 'info', duration : 30000, message : [ '', '' + sektionsLocalizedData.i18n[ "You've reached the maximum number of allowed nested sections." ] + '', '' ].join('') }); return; } // insert the nested section in the collection at the right place var presetColumnOrSectionCollection; self.updAPISetParams.sectionInjectPromise = $.Deferred(); var _doWhenPrebuiltSectionCollectionFetched = function( presetColumnOrSectionCollection ) { self.preparePresetSectionForInjection( presetColumnOrSectionCollection ) .fail( function( _er_ ){ self.updAPISetParams.promise.reject( 'updateAPISetting => error when preparePresetSectionForInjection => ' + params.action + ' => ' + _er_ ); // Used when updating the setting // @see end of this method self.updAPISetParams.sectionInjectPromise.reject( _er_ ); }) .done( function( maybeMultiSectionReadyToInject ) { var _injectNestedSectionInParentColumn = function( sectionReadyToInject, positionIndexInColumn ) { positionIndexInColumn = positionIndexInColumn || 0; // The following param "collection_of_preset_section_id" has been introduced when implementing support for multi-section pre-build sections // @see https://github.com/presscustomizr/nimble-builder/issues/489 // It is sent to the preview with ::reactToPreviewMsg, see bottom of the method. var injected_section_id = sektionsLocalizedData.prefixForSettingsNotSaved + self.guid(); params.collection_of_preset_section_id = params.collection_of_preset_section_id || []; params.collection_of_preset_section_id.push( injected_section_id ); columnCandidate.collection.splice( positionIndexInColumn, 0, { id : injected_section_id, level : 'section', collection : sectionReadyToInject.collection, options : sectionReadyToInject.options || {}, is_nested : true, ver_ini : sektionsLocalizedData.nimbleVersion }); }; // support for pre-built multi-section has been introduced in july 2019 // @see https://github.com/presscustomizr/nimble-builder/issues/489 var is_multi_section = 'section' === maybeMultiSectionReadyToInject.collection[0].level; if ( is_multi_section ) { _.each( maybeMultiSectionReadyToInject.collection, function( sectionReadyToInject ) { _injectNestedSectionInParentColumn( sectionReadyToInject, positionIndexInColumn ); positionIndexInColumn++; }); } else { _injectNestedSectionInParentColumn( maybeMultiSectionReadyToInject, positionIndexInColumn ); } // Used when updating the setting // @see end of this method self.updAPISetParams.sectionInjectPromise.resolve(); });//self.preparePresetSectionForInjection.done() };//_doWhenPrebuiltSectionCollectionFetched // Try to fetch the sections from the server // if sucessfull, resolve self.updAPISetParams.sectionInjectPromise.promise() self.getPresetSectionCollectionData({ is_user_section : params.is_user_section, presetSectionId : params.content_id }) .fail( function() { api.errare( 'updateAPISetting => ' + params.action + ' => Error with self.getPresetSectionCollectionData()', _er_ ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => Error with self.getPresetSectionCollectionData()'); }) .done( function( presetColumnOrSectionCollection ) { if ( ! _.isObject( presetColumnOrSectionCollection ) || _.isEmpty( presetColumnOrSectionCollection ) ) { api.errare( 'updateAPISetting => ' + params.action + ' => preset section type not found or empty : ' + params.content_id, presetColumnOrSectionCollection ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => preset section type not found or empty'); } // OK. time to resolve self.updAPISetParams.sectionInjectPromise.promise() _doWhenPrebuiltSectionCollectionFetched( presetColumnOrSectionCollection ); });//self.getPresetSectionCollectionData().done() } });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData, serverControlParams //self.updAPISetParams = { // params : params, // promise : $.Deferred(), // newSetValue : _.isObject( _currentSetValue ) ? $.extend( true, {}, _currentSetValue ) : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ), // cloneId : '', // sectionInjectPromise // }; var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { _updAPISet_sek_add_column : function() { var self = this, params, sektionCandidate; params = self.updAPISetParams.params; // an id must be provided if ( _.isEmpty( params.id ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing id' ); } sektionCandidate = self.getLevelModel( params.in_sektion, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == sektionCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent sektion matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent sektion matched'); return; } sektionCandidate.collection = _.isArray( sektionCandidate.collection ) ? sektionCandidate.collection : []; // can we add another column ? if ( ( self.MAX_NUMBER_OF_COLUMNS - 1 ) < _.size( sektionCandidate.collection ) ) { self.updAPISetParams.promise.reject( sektionsLocalizedData.i18n["You've reached the maximum number of columns allowed in this section."]); return; } // RESET ALL COLUMNS WIDTH // _.each( sektionCandidate.collection, function( colModel ) { // colModel.width = ''; // }); self.resetColumnsWidthInSection( sektionCandidate ); sektionCandidate.collection.push({ id : params.id, level : 'column', collection : [], ver_ini : sektionsLocalizedData.nimbleVersion }); }, _updAPISet_sek_remove_column : function() { var self = this, params, sektionCandidate; params = self.updAPISetParams.params; sektionCandidate = self.getLevelModel( params.in_sektion, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' != sektionCandidate ) { // can we remove the column ? if ( 1 === _.size( sektionCandidate.collection ) ) { self.updAPISetParams.promise.reject( sektionsLocalizedData.i18n["A section must have at least one column."]); return; } sektionCandidate.collection = _.isArray( sektionCandidate.collection ) ? sektionCandidate.collection : []; sektionCandidate.collection = _.filter( sektionCandidate.collection, function( column ) { return column.id != params.id; }); // RESET ALL COLUMNS WIDTH // _.each( sektionCandidate.collection, function( colModel ) { // colModel.width = ''; // }); self.resetColumnsWidthInSection( sektionCandidate ); } else { api.errare( 'updateAPISetting => ' + params.action + ' => no parent sektion matched' ); } }, _updAPISet_sek_duplicate_column : function() { var self = this, params, sektionCandidate; params = self.updAPISetParams.params; // an id must be provided if ( _.isEmpty( params.id ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing id' ); } sektionCandidate = self.getLevelModel( params.in_sektion, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == sektionCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent sektion matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent sektion matched'); return; } sektionCandidate.collection = _.isArray( sektionCandidate.collection ) ? sektionCandidate.collection : []; // can we add another column ? if ( ( self.MAX_NUMBER_OF_COLUMNS - 1 ) < _.size( sektionCandidate.collection ) ) { self.updAPISetParams.promise.reject( sektionsLocalizedData.i18n["You've reached the maximum number of columns allowed in this section."]); return; } var deepClonedColumn; try { deepClonedColumn = self.cloneLevel( params.id ); } catch( er ) { api.errare( 'updateAPISetting => ' + params.action, er ); return; } // items id of multi-items module must always be unique // this recursive method sniff and does the job self.maybeGenerateNewItemIdsForCrudModules( deepClonedColumn ); var _position = self.getLevelPositionInCollection( params.id, self.updAPISetParams.newSetValue.collection ); self.updAPISetParams.cloneId = deepClonedColumn.id;//will be passed in resolve() sektionCandidate.collection.splice( parseInt( _position + 1, 10 ), 0, deepClonedColumn ); // RESET ALL COLUMNS WIDTH // _.each( sektionCandidate.collection, function( colModel ) { // colModel.width = ''; // }); self.resetColumnsWidthInSection( sektionCandidate ); }, _updAPISet_sek_resize_column : function() { var self = this, params; params = self.updAPISetParams.params; if ( params.col_number < 2 ) return; var resizedColumn = self.getLevelModel( params.resized_column, self.updAPISetParams.newSetValue.collection ), sistercolumn = self.getLevelModel( params.sister_column, self.updAPISetParams.newSetValue.collection ); //api.infoLog( 'updateAPISetting => ' + params.action + ' => ', params ); // SET RESIZED COLUMN WIDTH if ( 'no_match' == resizedColumn ) { api.errare( 'updateAPISetting => ' + params.action + ' => no resized column matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no resized column matched'); return; } var _getColumnWidth = function( _candidate_ ) { var _width = '_not_set_'; var _options = _.isObject( _candidate_.options ) ? _candidate_.options : {}; if ( ! _.isEmpty( _options ) && _options.width && _options.width['custom-width'] ) { _width = parseFloat( _options.width['custom-width'] * 1 ); } return _width; }; var _setColumnWidth = function( _candidate_, newWidthValue ) { // start from a deep cloned object // important => fixes https://github.com/presscustomizr/nimble-builder/issues/455 var _new_options_values = $.extend( true, {}, _candidate_.options || {} ); // april 2020 : due to a previous implementation of width option as an array, there could be a bug when trying to set the width // because the check on _.isObject() was true for an array // Fixes https://github.com/presscustomizr/nimble-builder/issues/620 _new_options_values.width = ( _.isObject( _new_options_values.width ) && _new_options_values.width['custom-width'] ) ? _new_options_values.width : {}; _new_options_values.width['custom-width'] = newWidthValue; _candidate_.options = _new_options_values; // Live update the input value ( when rendered ) self.cachedElements.$body.find('[data-sek-width-range-column-id="'+ _candidate_.id +'"]').val( newWidthValue ).trigger('input', { is_resize_column_trigger : true } ); return newWidthValue; }; // DEPRECATED SINCE JUNE 2019 => resizedColumn.width = parseFloat( params.resizedColumnWidthInPercent ); var resizedColumnWidthInPercent = _setColumnWidth( resizedColumn, parseFloat( params.resizedColumnWidthInPercent ) ); // cast to number resizedColumnWidthInPercent = parseFloat( resizedColumnWidthInPercent ); // SET OTHER COLUMNS WIDTH var parentSektion = self.getLevelModel( params.in_sektion, self.updAPISetParams.newSetValue.collection ); var otherColumns = _.filter( parentSektion.collection, function( _col_ ) { return _col_.id != resizedColumn.id && _col_.id != sistercolumn.id; }); var otherColumnsWidth = parseFloat( resizedColumnWidthInPercent.toFixed(3) ); if ( ! _.isEmpty( otherColumns ) ) { _.each( otherColumns, function( colModel ) { currentColWidth = _getColumnWidth( colModel ); if ( '_not_set_' === currentColWidth || ! _.isNumber( currentColWidth * 1 ) || _.isEmpty( currentColWidth + '' ) || 1 > currentColWidth ) { // DEPRECATED SINCE JUNE 2019 => colModel.width = parseFloat( ( 100 / params.col_number ).toFixed(3) ); currentColWidth = _setColumnWidth( colModel, parseFloat( ( 100 / params.col_number ).toFixed(3) ) ); } // sum up all other column's width, excluding the resized and sister one. otherColumnsWidth = parseFloat( ( otherColumnsWidth + currentColWidth ).toFixed(3) ); }); } // SET SISTER COLUMN WIDTH // sum up all other column's width, excluding the resized and sister one. // api.infoLog( "resizedColumn.width", resizedColumn.width ); // api.infoLog( "otherColumns", otherColumns ); // then calculate the sistercolumn so we are sure that we feel the entire space of the sektion // DEPRECATED SINCE JUNE 2019 => sistercolumn.width = parseFloat( ( 100 - otherColumnsWidth ).toFixed(3) ); _setColumnWidth( sistercolumn, parseFloat( ( 100 - otherColumnsWidth ).toFixed(3) ) ); // api.infoLog('otherColumnsWidth', otherColumnsWidth ); // api.infoLog("sistercolumn.width", sistercolumn.width ); // api.infoLog( "parseFloat( ( 100 - otherColumnsWidth ).toFixed(3) )" , parseFloat( ( 100 - otherColumnsWidth ).toFixed(3) ) ); //api.infoLog('COLLECTION AFTER UPDATE ', parentSektion.collection ); }, _updAPISet_sek_move_column : function() { var self = this, params, sektionCandidate; params = self.updAPISetParams.params; var toSektionCandidate = self.getLevelModel( params.to_sektion, self.updAPISetParams.newSetValue.collection ), movedColumnCandidate, copyOfMovedColumnCandidate; if ( _.isEmpty( toSektionCandidate ) || 'no_match' == toSektionCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing target sektion' ); } if ( params.from_sektion != params.to_sektion ) { // Remove the moved column from the source sektion var fromSektionCandidate = self.getLevelModel( params.from_sektion, self.updAPISetParams.newSetValue.collection ); if ( _.isEmpty( fromSektionCandidate ) || 'no_match' == fromSektionCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing source column' ); } fromSektionCandidate.collection = _.isArray( fromSektionCandidate.collection ) ? fromSektionCandidate.collection : []; // Make a copy of the column candidate now, before removing it movedColumnCandidate = self.getLevelModel( params.id, fromSektionCandidate.collection ); copyOfMovedColumnCandidate = $.extend( true, {}, movedColumnCandidate ); // remove the column from its previous sektion fromSektionCandidate.collection = _.filter( fromSektionCandidate.collection, function( column ) { return column.id != params.id; }); // Reset the column's width in the target sektion // _.each( fromSektionCandidate.collection, function( colModel ) { // colModel.width = ''; // }); self.resetColumnsWidthInSection( fromSektionCandidate ); } // update the target sektion toSektionCandidate.collection = _.isArray( toSektionCandidate.collection ) ? toSektionCandidate.collection : []; originalCollection = $.extend( true, [], toSektionCandidate.collection ); reorderedCollection = []; _.each( params.newOrder, function( _id_ ) { // in the case of a cross sektion movement, we need to add the moved column to the target sektion if ( params.from_sektion != params.to_sektion && _id_ == copyOfMovedColumnCandidate.id ) { reorderedCollection.push( copyOfMovedColumnCandidate ); } else { columnCandidate = self.getLevelModel( _id_, originalCollection ); if ( _.isEmpty( columnCandidate ) || 'no_match' == columnCandidate ) { throw new Error( 'updateAPISetting => moveColumn => missing columnCandidate' ); } reorderedCollection.push( columnCandidate ); } }); toSektionCandidate.collection = reorderedCollection; // Reset the column's width in the target sektion // _.each( toSektionCandidate.collection, function( colModel ) { // colModel.width = ''; // }); self.resetColumnsWidthInSection( toSektionCandidate ); }, });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData, serverControlParams //self.updAPISetParams = { // params : params, // promise : $.Deferred(), // newSetValue : _.isObject( _currentSetValue ) ? $.extend( true, {}, _currentSetValue ) : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ), // cloneId : '', // sectionInjectPromise // }; var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { _updAPISet_sek_add_module : function() { var self = this, params, columnCandidate, startingModuleValue,// will be populated by the optional starting value specificied on module registration position;//<= the position of the module or section or nested section in a column or location params = self.updAPISetParams.params; // an id must be provided if ( _.isEmpty( params.id ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing id' ); } // a module_type must be provided if ( _.isEmpty( params.module_type ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing module_type' ); } columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' === columnCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent column matched'); return; } position = 0; columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; // get the position of the before or after module or nested section _.each( columnCandidate.collection, function( moduleOrNestedSectionModel, index ) { if ( params.before_module_or_nested_section === moduleOrNestedSectionModel.id ) { position = index; } if ( params.after_module_or_nested_section === moduleOrNestedSectionModel.id ) { position = index + 1; } }); var _moduleParams = { id : params.id, level : 'module', module_type : params.module_type, ver_ini : sektionsLocalizedData.nimbleVersion }; // Let's add the starting value if provided when registrating the module startingModuleValue = self.getModuleStartingValue( params.module_type ); if ( 'no_starting_value' !== startingModuleValue ) { _moduleParams.value = startingModuleValue; } columnCandidate.collection.splice( position, 0, _moduleParams ); }, _updAPISet_sek_duplicate_module : function() { var self = this, params, columnCandidate; params = self.updAPISetParams.params; // an id must be provided if ( _.isEmpty( params.id ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing id' ); } columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' == columnCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => no parent column matched'); return; } columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; var deepClonedModule; try { deepClonedModule = self.cloneLevel( params.id ); } catch( er ) { api.errare( 'updateAPISetting => ' + params.action, er ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => error when cloning the level'); return; } // items id of multi-items module must always be unique // this recursive method sniff and does the job self.maybeGenerateNewItemIdsForCrudModules( deepClonedModule ); var insertInposition = self.getLevelPositionInCollection( params.id, self.updAPISetParams.newSetValue.collection ); self.updAPISetParams.cloneId = deepClonedModule.id;//will be passed in resolve() columnCandidate.collection.splice( parseInt( insertInposition + 1, 10 ), 0, deepClonedModule ); }, _updAPISet_sek_remove_module : function() { var self = this, params, columnCandidate; params = self.updAPISetParams.params; // an id must be provided if ( _.isEmpty( params.id ) ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing id' ); } columnCandidate = self.getLevelModel( params.in_column, self.updAPISetParams.newSetValue.collection ); if ( 'no_match' != columnCandidate ) { columnCandidate.collection = _.isArray( columnCandidate.collection ) ? columnCandidate.collection : []; columnCandidate.collection = _.filter( columnCandidate.collection, function( module ) { return module.id != params.id; }); } else { api.errare( 'updateAPISetting => ' + params.action + ' => no parent column matched' ); } }, _updAPISet_sek_move_module : function() { var self = this, params; params = self.updAPISetParams.params; var toColumnCandidate, movedModuleCandidate, copyOfMovedModuleCandidate; // loop on the sektions to find the toColumnCandidate // _.each( self.updAPISetParams.newSetValue.collection, function( _sektion_ ) { // _.each( _sektion_.collection, function( _column_ ) { // if ( _column_.id == params.to_column ) { // toColumnCandidate = _column_; // } // }); // }); toColumnCandidate = self.getLevelModel( params.to_column, self.updAPISetParams.newSetValue.collection ); if ( _.isEmpty( toColumnCandidate ) || 'no_match' == toColumnCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing target column' ); } // If the module has been moved to another column // => remove the moved module from the source column if ( params.from_column != params.to_column ) { var fromColumnCandidate; fromColumnCandidate = self.getLevelModel( params.from_column, self.updAPISetParams.newSetValue.collection ); if ( _.isEmpty( fromColumnCandidate ) || 'no_match' == fromColumnCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing source column' ); } fromColumnCandidate.collection = _.isArray( fromColumnCandidate.collection ) ? fromColumnCandidate.collection : []; // Make a copy of the module candidate now, before removing it movedModuleCandidate = self.getLevelModel( params.id, self.updAPISetParams.newSetValue.collection ); copyOfMovedModuleCandidate = $.extend( true, {}, movedModuleCandidate ); // remove the module from its previous column fromColumnCandidate.collection = _.filter( fromColumnCandidate.collection, function( module ) { return module.id != params.id; }); }// if params.from_column != params.to_column // update the target column toColumnCandidate.collection = _.isArray( toColumnCandidate.collection ) ? toColumnCandidate.collection : []; originalCollection = $.extend( true, [], toColumnCandidate.collection ); var reorderedCollection = []; _.each( params.newOrder, function( _id_ ) { if ( params.from_column != params.to_column && _id_ == copyOfMovedModuleCandidate.id ) { reorderedCollection.push( copyOfMovedModuleCandidate ); } else { moduleCandidate = self.getLevelModel( _id_, self.updAPISetParams.newSetValue.collection ); if ( _.isEmpty( moduleCandidate ) || 'no_match' == moduleCandidate ) { throw new Error( 'updateAPISetting => ' + params.action + ' => missing moduleCandidate' ); } reorderedCollection.push( moduleCandidate ); } }); // Check if we have duplicates ? if ( reorderedCollection.length != _.uniq( reorderedCollection ).length ) { throw new Error( 'updateAPISetting => ' + params.action + ' => there are duplicated modules in column : ' + toColumnCandidate.id ); } else { toColumnCandidate.collection = reorderedCollection; } }, _updAPISet_sek_set_module_value : function() { var self = this, params, moduleCandidate; params = self.updAPISetParams.params; moduleCandidate = self.getLevelModel( params.id, self.updAPISetParams.newSetValue.collection ); // Is this a multi-item module ? // Fixes https://github.com/presscustomizr/nimble-builder/issues/616 var _ctrl_ = params.settingParams.args.moduleRegistrationParams.control, _module_id_ = params.settingParams.args.moduleRegistrationParams.id, parentModuleInstance = _ctrl_.czr_Module( _module_id_ ); if ( ! _.isEmpty( parentModuleInstance ) ) { isMultiItemModule = parentModuleInstance.isMultiItem(); } else { api.errare( 'updateAPISetting => missing parentModuleInstance', params ); } // if multi-item module, the value is array of items, otherwise an object var _modValueCandidate = isMultiItemModule ? [] : {}; // consider only the non empty settings for db // booleans should bypass this check _.each( params.value || (isMultiItemModule ? [] : {}), 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; _modValueCandidate[ _key_ ] = _val_; }); if ( 'no_match' == moduleCandidate ) { api.errare( 'updateAPISetting => ' + params.action + ' => no module matched', params ); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => error no module matched'); return; } if ( _.isEmpty( params.options_type ) ) { api.errare( 'updateAPISetting => ' + params.action + ' => missing options_type'); self.updAPISetParams.promise.reject( 'updateAPISetting => ' + params.action + ' => missing options_type'); return; } // Is this a father module ? // If yes, the module value is structured by option group, each option group being updated by a child module // If no, the default option type is : '__no_option_group_to_be_updated_by_children_modules__' if ( '__no_option_group_to_be_updated_by_children_modules__' === params.options_type ) { moduleCandidate.value = _modValueCandidate; } else { // start from a deep cloned object // prevents issues like https://github.com/presscustomizr/nimble-builder/issues/455 var _new_module_values = $.extend( true, {}, _.isEmpty( moduleCandidate.value ) ? {} : moduleCandidate.value ); _new_module_values[ params.options_type ] = _modValueCandidate; moduleCandidate.value = _new_module_values; } }, });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData, serverControlParams //self.updAPISetParams = { // params : params, // promise : $.Deferred(), // newSetValue : _.isObject( _currentSetValue ) ? $.extend( true, {}, _currentSetValue ) : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ), // cloneId : '', // sectionInjectPromise // }; var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { //------------------------------------------------------------------------------------------------- //-- FILE IMPORT //------------------------------------------------------------------------------------------------- _updAPISet_sek_import_from_file : function() { var self = this, params; params = self.updAPISetParams.params; //api.infoLog( 'sek-import-from-file', params ); if ( _.isUndefined( params.imported_content.data ) || _.isUndefined( params.imported_content.metas ) ) { api.errare( 'updateAPISetting::sek-import-from-file => invalid imported content', imported_content ); return; } var importedCollection = _.isArray( params.imported_content.data.collection ) ? $.extend( true, [], params.imported_content.data.collection ) : []; // ASSIGN MISSING LOCATIONS => IF IMPORTED LOCATIONS DON'T MATCH CURRENT PAGE LOCATIONS // NB will import sections in the first active location of the page // Important : header and footer must be excluded from active locations // // For example the current page has only the 'loop_start' location, whereas the imported content includes 3 locations : // - after_header // - loop_start // - before_footer // Among those 3 locations, 2 are not active in the page. // We will merge all section collections from the 3 imported locations one new collection, that will be assigned to 'loop_start' // Note that the active imported locations are ordered like they were on the page when exported. // // So : // 1) identify the first active location of the page // 2) populate a new collection of combined sections from all active imported locations. // 3) updated the imported collection with this if ( true === params.assign_missing_locations ) { var importedActiveLocations = params.imported_content.metas.active_locations, allActiveLocations = api.czr_sektions.activeLocations(), currentActiveLocations = []; // Set the current active locations excluding header and footer location _.each( allActiveLocations, function( loc_id ) { if( !self.isHeaderLocation( loc_id ) && !self.isFooterLocation( loc_id ) ) { currentActiveLocations.push(loc_id); } }); // console.log('Current set value ?', api( _collectionSettingId_ )() ); // console.log('import params', params ); // console.log('importedCollection?', importedCollection ); // console.log('importedActiveLocations', importedActiveLocations ); // first active location of the current setting var firstCurrentActiveLocationId = _.first( currentActiveLocations ); if ( !_.isEmpty( firstCurrentActiveLocationId ) && !_.isEmpty( importedActiveLocations ) && _.isArray( importedActiveLocations ) ) { // importedActiveLocationsNotAvailableInCurrentActiveLocations // Example : // active location in the page : loop_start, loop_end // active locations imported : after_header, loop_start, before_footer // importedActiveLocationsNotAvailableInCurrentActiveLocations => after_header, before_footer var importedActiveLocationsNotAvailableInCurrentActiveLocations = $(importedActiveLocations).not(currentActiveLocations).get(), firstCurrentLocationData = self.getLevelModel( firstCurrentActiveLocationId, self.updAPISetParams.newSetValue.collection ), importedTargetLocationData = self.getLevelModel( firstCurrentActiveLocationId, params.imported_content.data.collection ), newCollectionForTargetLocation = [];// the collection that will hold the merge of all active imported collections // normalize // => make sure we have a collection array, even empty firstCurrentLocationData.collection = _.isArray( firstCurrentLocationData.collection ) ? firstCurrentLocationData.collection : []; importedTargetLocationData.collection = _.isArray( importedTargetLocationData.collection ) ? importedTargetLocationData.collection : []; // loop on the active imported locations // Example : ["__after_header", "__before_main_wrapper", "loop_start", "__before_footer"] // and populate newCollectionForTargetLocation, with locations ordered as they were on export // importedCollection is a clone _.each( importedActiveLocations, function( impLocationId ){ var impLocationData = self.getLevelModel( impLocationId, importedCollection ); if ( _.isEmpty( impLocationData.collection ) ) return; newCollectionForTargetLocation = _.union( newCollectionForTargetLocation, impLocationData.collection ); });//_.each( importedActiveLocations // replace the previous collection of the target location, by the union of all collections. // for example, if 'loop_start' is the target location, all sections will be added to it. importedTargetLocationData.collection = newCollectionForTargetLocation; // remove the missing locations from the imported collection // importedActiveLocationsNotAvailableInCurrentActiveLocations params.imported_content.data.collection = _.filter( params.imported_content.data.collection, function( _location ) { return !_.contains( importedActiveLocationsNotAvailableInCurrentActiveLocations, _location.id ); }); }//if ( !_.isEmpty( firstCurrentActiveLocationId ) ) }//if ( true === params.assign_missing_locations ) // SHALL WE MERGE ? // Sept 2019 note : for local import only. Not implemented for global https://github.com/presscustomizr/nimble-builder/issues/495 // loop on each location of the imported content // if the current setting value has sections in a location, add them before the imported ones // keep_existing_sections is a user check option // @see PHP sek_get_module_params_for_sek_local_imp_exp() if ( true === params.keep_existing_sections ) { // note that importedCollection is a unlinked clone of params.imported_content.data.collection // merge sections _.each( importedCollection, function( imp_location_data ) { var currentLocationData = self.getLevelModel( imp_location_data.id, self.updAPISetParams.newSetValue.collection ); if ( _.isEmpty( currentLocationData.collection ) ) return; var importedLocationData = self.getLevelModel( imp_location_data.id, params.imported_content.data.collection ); importedLocationData.collection = _.union( currentLocationData.collection, importedLocationData.collection ); }); // merge fonts if needed var currentFonts = self.updAPISetParams.newSetValue.fonts, importedFonts = params.imported_content.data.fonts; if ( currentFonts && !_.isEmpty( currentFonts ) && _.isArray( currentFonts ) ) { importedFonts = _.isArray( importedFonts ) ? importedFonts : []; // merge and remove duplicated fonts params.imported_content.data.fonts = _.uniq( _.union( currentFonts, importedFonts ) ); } }// if true === params.merge self.updAPISetParams.newSetValue = params.imported_content.data; } });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData, serverControlParams //self.updAPISetParams = { // params : params, // promise : $.Deferred(), // newSetValue : _.isObject( _currentSetValue ) ? $.extend( true, {}, _currentSetValue ) : self.getDefaultSektionSettingValue( params.is_global_location ? 'global' : 'local' ), // cloneId : '', // sectionInjectPromise // }; var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { //------------------------------------------------------------------------------------------------- //-- INJECT FROM TMPL GALLERY //------------------------------------------------------------------------------------------------- // self.updAPISetParams.params : { // action: "sek-inject-tmpl-from-gallery" // assign_missing_locations: undefined // cloneId: "" // injected_content: {data: {…}, metas: {…}, img_errors: Array(0)} // is_global_location: false // scope: "local" // tmpl_inject_mode: "replace" // } _updAPISet_sek_inject_tmpl_from_gallery : function() { var self = this, params; params = self.updAPISetParams.params; //api.infoLog( 'api update params for sek-inject-tmpl-from-gallery', params ); // DO WE HAVE PROPER CONTENT DO INJECT ? if ( _.isUndefined( params.injected_content.data ) || _.isUndefined( params.injected_content.metas ) ) { api.errare( 'updateAPISetting::sek-inject-tmpl-from-gallery => invalid imported content', injected_content ); return; } //------------------------------------------------------------------------------------------------- //-- HELPERS //------------------------------------------------------------------------------------------------- var _allTmplLocationsExistLocally = function() { var bool = true; _.each( tmplLocations, function( loc_id ){ if (!bool) return; bool = _.contains(localLocations, loc_id); }); return bool; }; // @return bool var _isTmplHeaderLocId = function( loc_id ) { return params.injected_content && params.injected_content.metas && loc_id === params.injected_content.metas.tmpl_header_location; }; // @return bool var _isTmplFooterLocId = function( loc_id ) { return params.injected_content && params.injected_content.metas && loc_id === params.injected_content.metas.tmpl_footer_location; }; // The template has a header/footer if we find the header or the footer location // AND // if there's a local_header_footer property set in the local_options var _hasTmplHeaderFooter = function() { var hasHeaderFooterLoc = false; _.each( tmplLocations, function( loc_id ){ if (hasHeaderFooterLoc) return; if ( _isTmplHeaderLocId( loc_id ) || _isTmplFooterLocId( loc_id ) ) { hasHeaderFooterLoc = self.getLevelModel( loc_id, tmplCollection ); hasHeaderFooterLoc = 'no_match' != hasHeaderFooterLoc; } }); return hasHeaderFooterLoc && !_.isEmpty( tmplLocalOptions.local_header_footer ); }; var _tmplUsesNBtemplate = function() { return tmplLocalOptions && tmplLocalOptions.template && 'nimble_template' === tmplLocalOptions.template.local_template; }; var tmplCollection = _.isArray( params.injected_content.data.collection ) ? $.extend( true, [], params.injected_content.data.collection ) : [], tmplLocations = params.injected_content.metas.tmpl_locations, localLocations = [], currentSettingCollection = self.updAPISetParams.newSetValue.collection; // Set the current local locations, make sure we exclude all global locations _.each( api.czr_sektions.activeLocations(), function( loc_id ) { if( !self.isGlobalLocationId(loc_id) ) { localLocations.push(loc_id); } }); // Imported Active Locations has to be an array not empty if ( !_.isArray(tmplLocations) || _.isEmpty(tmplLocations) ) { api.errare( 'updateAPISetting::sek-inject-tmpl-from-gallery => invalid imported template locations', params ); return; } // TEMPLATE LOCAL OPTIONS and FONTS // Important : // - Local options is structured as an object : { local_header_footer: {…}, widths: {…}} }. But when not populated, it can be an array []. So make sure the type if set as object before merging it with current page local options // - Fonts is a collection described with an array var tmplLocalOptions = params.injected_content.data.local_options; tmplLocalOptions = $.extend( true, {}, _.isObject( tmplLocalOptions ) ? tmplLocalOptions : {} ); var tmplFonts = params.injected_content.data.fonts; tmplFonts = _.isArray( tmplFonts ) ? $.extend( true, [], tmplFonts ) : []; // Define variables uses for all cases var newSetValueCollection = $.extend( true, [], currentSettingCollection ),// Create a deep copy of the current API collection _allContentSectionsInTmpl = [], targetLocationId = '__not_set__', locModel, targetLocationModel, tmplLocCandidate, localLocCandidate; // Gather all template content sections from potentially multiple locations in one collection // => header and footer locations are excluded from this collection // This collection is used : // - in 'replace' mode when template locations don't exists in the local context // - in 'before' and 'after' mode // Note : if this collection is used the template header and footer ( if any ) have to be added separately _.each( tmplCollection, function( loc_data ){ if ( _isTmplHeaderLocId( loc_data.id ) || _isTmplFooterLocId( loc_data.id ) ) return; if( !_.isEmpty( loc_data.collection ) ) { _allContentSectionsInTmpl = _.union( _allContentSectionsInTmpl, loc_data.collection ); } }); // console.log('_hasTmplHeaderFooter ?', _hasTmplHeaderFooter() ); // console.log('_allContentSectionsInTmpl ?', _allContentSectionsInTmpl); // console.log('NEW SET VALUE COLLECTION? ', $.extend( true, [], newSetValueCollection ) ); // If the current page already has NB sections, the user can chose 3 options : REPLACE, BEFORE, AFTER. // when the page has no NB sections, the default option is REPLACE switch( params.tmpl_inject_mode ) { //------------------------------------------------------------------------------------------------- //-- REPLACE CASE ( default case ) //------------------------------------------------------------------------------------------------- case 'replace' : // api.infoLog('CURRENT SETTING VALUE ?', self.updAPISetParams.newSetValue ); // console.log('SO COLLECTION BEFORE ?', tmplCollection ); // return bool // IF ALL TEMPLATE LOCATIONS EXIST IN CURRENT PAGE // Loop on local locations, use template locations when exists, otherwise use local ones if ( _allTmplLocationsExistLocally() ) { // Replace locations from local collection that are provided by the tmpl, and not empty // => if the header / footer template location is empty, keep the local one // => the tmpl location will replace the local location in the collection newSetValueCollection = []; var resetLocalLocation, newLocalLocation; _.each( currentSettingCollection, function( _localLocation ) { tmplLocCandidate = _.findWhere(tmplCollection, { id : _localLocation.id }) || {}; if ( _.isEmpty( tmplLocCandidate.collection ) ) { if ( self.isHeaderLocation( _localLocation.id ) || self.isFooterLocation( _localLocation.id ) ) { newSetValueCollection.push( _localLocation ); } else { // Reset previous local location to defaults resetLocalLocation = { collection : [], options :[] }; newLocalLocation = $.extend( true, {}, _localLocation ); newLocalLocation = $.extend( newLocalLocation, resetLocalLocation ); newSetValueCollection.push( newLocalLocation ); } } else { newSetValueCollection.push( tmplLocCandidate ); } }); // console.log('tmplCollection ??', tmplCollection ); // console.log('localLocations ??', localLocations); } else { // IF TEMPLATE LOCATIONS DO NOT MATCH THE ONES OF THE CURRENT PAGE => ASSIGN ALL TEMPLATE SECTIONS TO LOOP_START OR First local content location if ( _tmplUsesNBtemplate() ) { targetLocationId = 'loop_start'; } else { if ( _.contains(localLocations, 'loop_start') ) { targetLocationId = 'loop_start'; } else { _.each( localLocations, function( loc_id ) { if ( !self.isHeaderLocation( loc_id ) && !self.isFooterLocation( loc_id ) ) { targetLocationId = loc_id; } }); } } // At this point, we need a target location id if ( '__not_set__' === targetLocationId ) { api.errare( 'updateAPISetting::sek-inject-tmpl-from-gallery => target location id is empty' ); break; } // Get the current target location model targetLocationModel = self.getLevelModel( targetLocationId, newSetValueCollection ); if ( 'no_match' === targetLocationModel ) { api.errare('::_updAPISet_sek_inject_tmpl_from_gallery => error => target location id ' + targetLocationId ); break; } targetLocationModel = $.extend( true, {}, targetLocationModel );// <= create a deep copy // Replace the target location collection with the template one targetLocationModel.collection = _allContentSectionsInTmpl; // remove all locations from future setting value newSetValueCollection = []; // If the template has a header/footer use it // else, if a header footer is defined locally if ( _hasTmplHeaderFooter() ) { _.each( tmplLocations, function( loc_id ) { if ( _isTmplHeaderLocId( loc_id ) || _isTmplFooterLocId( loc_id ) ) { tmplLocCandidate = self.getLevelModel( loc_id, tmplCollection ); if ( 'no_match' === tmplLocCandidate ) { api.errare('::_updAPISet_sek_inject_tmpl_from_gallery => error => location id ' + loc_id +' not found in template collection'); return; } else { newSetValueCollection.push( tmplLocCandidate ); } } }); } // Populate the local target location with the template section collection // AND // Re-populate the header and footer location, either with the local one, or the template one ( if any) _.each( localLocations, function( loc_id ) { if ( targetLocationId === loc_id ) { newSetValueCollection.push( targetLocationModel ); } localLocModel = self.getLevelModel( loc_id, currentSettingCollection ); if ( 'no_match' === localLocModel ) { api.errare('::_updAPISet_sek_inject_tmpl_from_gallery => error => location id ' + loc_id +' not found in current setting collection'); return; } // re-add header and footer if _hasTmplHeaderFooter() if ( !_hasTmplHeaderFooter() ) { if ( self.isHeaderLocation( loc_id ) || self.isFooterLocation( loc_id ) ) { newSetValueCollection.push( localLocModel ); } } }); } break; //------------------------------------------------------------------------------------------------- //-- INJECT BEFORE CASE //------------------------------------------------------------------------------------------------- case 'before' : // For the before case, we are sure that hasCurrentPageNBSectionsNotHeaderFooter() is true // so there's at least one location that has section(s) // Find the first non header/footer location not empty _.each( localLocations, function( loc_id ){ // stop if the location id has been found if ( '__not_set__' != targetLocationId ) return; locModel = self.getLevelModel( loc_id, newSetValueCollection ); if ( 'no_match' === locModel ) { api.errare('::_updAPISet_sek_inject_tmpl_from_gallery => error => location id not found' + loc_id ); return; } if ( !self.isHeaderLocation( loc_id ) && !self.isFooterLocation( loc_id ) ) { if ( !_.isEmpty( locModel.collection ) ) { targetLocationId = loc_id; targetLocationModel = locModel; } } }); // At this point, we need a target location id if ( '__not_set__' === targetLocationId ) { api.errare( 'updateAPISetting::sek-inject-tmpl-from-gallery => target location id is empty' ); break; } // Get the current target location model targetLocationModel = $.extend( true, {}, targetLocationModel ); // Adds the template sections BEFORE the existing sections of the target location targetLocationModel.collection = _.union( _allContentSectionsInTmpl, targetLocationModel.collection ); // remove all locations from future setting value newSetValueCollection = []; // Re-populate the location models previously removed the updated target location model _.each( localLocations, function( loc_id ){ if ( targetLocationId === loc_id ) { newSetValueCollection.push( targetLocationModel ); } else { if ( 'no_match' === locModel ) { api.errare('::_updAPISet_sek_inject_tmpl_from_gallery => error => location id not found' + loc_id ); return; } newSetValueCollection.push( self.getLevelModel( loc_id, currentSettingCollection ) ); } }); break; //------------------------------------------------------------------------------------------------- //-- INJECT AFTER CASE //------------------------------------------------------------------------------------------------- case 'after' : // For the after case, we are sure that hasCurrentPageNBSectionsNotHeaderFooter() is true // so there's at least one location that has section(s) // Find the last non header/footer location not empty _.each( localLocations.reverse(), function( loc_id ){ // stop if the location id has been found if ( '__not_set__' != targetLocationId ) return; locModel = self.getLevelModel( loc_id, newSetValueCollection ); if ( 'no_match' === locModel ) { api.errare('::_updAPISet_sek_inject_tmpl_from_gallery => error => location id not found' + loc_id ); return; } if ( !self.isHeaderLocation( loc_id ) && !self.isFooterLocation( loc_id ) ) { if ( !_.isEmpty( locModel.collection ) ) { targetLocationId = loc_id; targetLocationModel = locModel; } } }); // At this point, we need a target location id if ( '__not_set__' === targetLocationId ) { api.errare( 'updateAPISetting::sek-inject-tmpl-from-gallery => target location id is empty' ); break; } // Get the current target location model targetLocationModel = $.extend( true, {}, targetLocationModel ); // Adds the template sections AFTER the existing sections of the target location targetLocationModel.collection = _.union( targetLocationModel.collection, _allContentSectionsInTmpl ); // remove all locations from future setting value newSetValueCollection = []; // Re-populate the location models previously removed the updated target location model _.each( localLocations, function( loc_id ){ if ( targetLocationId === loc_id ) { newSetValueCollection.push( targetLocationModel ); } else { locModel = self.getLevelModel( loc_id, currentSettingCollection ); if ( 'no_match' === locModel ) { api.errare('::_updAPISet_sek_inject_tmpl_from_gallery => error => loc id not found' + loc_id ); return; } newSetValueCollection.push( locModel ); } }); break; } // update the API setting // this is a candiate setting value, the new setting value will be validated in ::updateAPISetting => ::validateSettingValue() self.updAPISetParams.newSetValue.collection = newSetValueCollection; // LOCAL OPTIONS // local_options states if the imported template uses nimble_template, or use custom_width, custom_css, performance, etc.. see the full list of local options in ::generateUIforLocalSkopeOptions // Design decision : by default NB extends existing local options with the imported ones. // import mode : // 'replace' (default) => local options extended // insert 'before' or 'after' => existing local options are preserved // // Scenario : // 1) user has created NB sections on a single post and wants to insert a NB template before the existing sections ( 'before' inject_mode ) // => in this case, we need to keep the default theme template, local options must be the existing ones => no extension of local options. // // 2) the current page has no NB sections yet, import mode is 'replace' by default // => it means that if the imported template uses NB template as canvas, it must be set in local options => extension of local options if ( !_.isEmpty( tmplLocalOptions ) && 'replace' === params.tmpl_inject_mode ) { var currentLocalOptions = self.updAPISetParams.newSetValue.local_options; currentLocalOptions = $.extend( true, {}, _.isObject( currentLocalOptions ) ? currentLocalOptions : {} ); self.updAPISetParams.newSetValue.local_options = _.extend( currentLocalOptions, tmplLocalOptions ); } // FONTS // If there are imported fonts, we need to merge when import mode is not 'replace', otherwise we need to copy the imported font collection in .fonts property of the API setting. if ( _.isArray( tmplFonts ) && !_.isEmpty( tmplFonts ) ) { if ( 'replace' != params.tmpl_inject_mode ) { var currentFonts = self.updAPISetParams.newSetValue.fonts; currentFonts = $.extend( true, [], _.isArray( currentFonts ) ? currentFonts : [] ); // merge two collection of fonts without duplicates self.updAPISetParams.newSetValue.fonts = _.uniq( _.union( tmplFonts, currentFonts )); } else { self.updAPISetParams.newSetValue.fonts = tmplFonts; } } //api.infoLog('SETTING VALUE AFTER ?', self.updAPISetParams.newSetValue ); }//_updAPISet_sek_inject_tmpl_from_gallery });//$.extend() })( wp.customize, jQuery );//global sektionsLocalizedData var CZRSeksPrototype = CZRSeksPrototype || {}; (function ( api, $ ) { $.extend( CZRSeksPrototype, { //@return void() // clean registered controls, sections, panels // only elements that have a true "track" param on registration are populated in the registered() collection // if the _id_ param is not specified, all registered controls, sections and panels are removed. // // preserve the settings => because this is where the customizer changeset of values is persisted before publishing // typically fired before updating the ui. @see ::generateUI() // // March 2021 => also clean large select options like fontPicker which generates thousands of lines and slow down the UI dramatically if kept cleanRegisteredAndLargeSelectInput : function( _id_ ) { var self = this, registered = $.extend( true, [], self.registered() || [] ); // added for https://github.com/presscustomizr/nimble-builder/issues/403 // in order to remove all instantiations of WP editor // @see ::initialize() api.trigger('sek-before-clean-registered'); registered = _.filter( registered, function( _reg_ ) { if ( 'setting' !== _reg_.what ) { if ( api[ _reg_.what ].has( _reg_.id ) ) { if ( ! _.isEmpty( _id_ ) && _reg_.id !== _id_ ) return; // fire an event before removal, can be used to clean some jQuery plugin instance for example if ( _.isFunction( api[ _reg_.what ]( _reg_.id ).trigger ) ) {//<= Section and Panel constructor are not extended with the Event class, that's why we check if this method exists self.trigger( 'sek-ui-pre-removal', { what : _reg_.what, id : _reg_.id } ); } $.when( api[ _reg_.what ]( _reg_.id ).container.remove() ).done( function() { // remove control, section, panel api[ _reg_.what ].remove( _reg_.id ); // useful event, used to destroy the $ drop plugin instance for the section / module picker self.trigger( 'sek-ui-removed', { what : _reg_.what, id : _reg_.id } ); }); } } return _reg_.what === 'setting'; }); self.registered( registered ); // March 2021 // clean font picker markup, which generates thousands of select options lines and slow down the entire UI when kept // This concerns the global options and local options for which controls are not cleaned like the one of the levels UI self.cachedElements.$body.find('[data-input-type="font_picker"]').each( function() { var currentInputVal = $(this).find('select[data-czrtype]').val(); // clean select 2 instance + all select options if ( !_.isUndefined( $(this).find('select[data-czrtype]').data('czrSelect2') ) ) { $(this).find('select[data-czrtype]').czrSelect2('destroy'); } $(this).find('select[data-czrtype]').html(''); // append the current input val $(this).find('select[data-czrtype]').html('').append( $('