( function( $ ) {
/**
* Helper for rendering builder settings forms.
*
* @since 2.0
* @class FLBuilderSettingsForms
*/
FLBuilderSettingsForms = {
/**
* Config for the current form that is rendering.
*
* @since 2.0
* @property {Object} config
*/
config : null,
/**
* Settings cache for the current form so we can compare
* later and see if settings have changed before saving.
*
* @since 2.0
* @property {Object} settings
*/
settings : null,
/**
* A reference to the AJAX object for rendering legacy settings.
*
* @since 2.0
* @property {Object} legacyXhr
*/
legacyXhr : null,
/**
* @since 2.0
* @method init
*/
init: function() {
this.bind();
},
/**
* @since 2.0
* @method bind
*/
bind: function() {
FLBuilder.addHook( 'didDeleteRow', this.closeOnDeleteNode );
FLBuilder.addHook( 'didDeleteColumn', this.closeOnDeleteNode );
FLBuilder.addHook( 'didDeleteModule', this.closeOnDeleteNode );
},
/**
* Renders a settings form.
*
* @since 2.0
* @method render
* @param {Object} config
* @param {Function} callback
*/
render: function( config, callback ) {
var forms = FLBuilderSettingsConfig.forms,
modules = FLBuilderSettingsConfig.modules,
defaults = {
type : 'general',
id : null,
nodeId : null,
className : '',
attrs : '',
title : '',
badges : [],
tabs : [],
buttons : [],
settings : {},
legacy : null,
rules : null,
preview : null,
helper : null
};
// Load settings from the server if we have a node but no settings.
if ( config.nodeId && ! config.settings ) {
this.loadNodeSettings( config, callback );
return;
}
// Merge the config into the defaults and make sure we have a callback.
config = $.extend( defaults, config );
callback = undefined === callback ? function(){} : callback;
// Add the form data to the config.
if ( ! config.id ) {
return;
} else if ( 'general' === config.type && undefined !== forms[ config.id ] ) {
config = $.extend( true, config, forms[ config.id ] );
} else if ( 'module' === config.type && undefined !== modules[ config.id ] ) {
config = $.extend( true, config, modules[ config.id ] );
} else {
return;
}
// Store the config so it can be accessed by forms.
this.config = config;
// Render the lightbox and form.
if ( this.renderLightbox( config ) ) {
// Finish rendering.
if ( config.legacy || ! this.renderLegacySettings( config, callback ) ) {
this.renderComplete( config, callback );
} else {
this.showLightboxLoader();
}
}
},
/**
* Loads node settings for a form if they do not exist in
* the settings config cache.
*
* @since 2.1
* @method loadNodeSettings
* @param {Object} config
* @param {Function} callback
* @return {Boolean}
*/
loadNodeSettings: function( config, callback ) {
FLBuilder.showAjaxLoader();
FLBuilder.ajax( {
action : 'get_node_settings',
node_id : config.nodeId,
}, function( response ) {
config.settings = JSON.parse( response );
FLBuilderSettingsConfig.nodes[ config.nodeId ] = config.settings;
FLBuilderSettingsForms.render( config, callback );
FLBuilder.hideAjaxLoader();
} );
},
/**
* Renders the lightbox for a settings form.
*
* @since 2.0
* @method renderLightbox
* @param {Object} config
* @return {Boolean}
*/
renderLightbox: function( config ) {
var template = wp.template( 'fl-builder-settings' ),
form = FLBuilder._lightbox._node.find( 'form.fl-builder-settings' ),
nested = $( '.fl-lightbox-wrap[data-parent]' );
// Don't render a node form if it's already open.
if ( config.nodeId && config.nodeId === form.data( 'node' ) && ! config.lightbox ) {
FLBuilder._focusFirstSettingsControl();
return false;
}
// Render the lightbox and form.
if ( ! config.lightbox ) {
// Save existing settings first if any exist. Don't proceed if it fails.
if ( ! FLBuilder._triggerSettingsSave( true, true ) ) {
return false;
}
// Cancel any preview refreshes.
if ( FLBuilder.preview ) {
FLBuilder.preview.cancel();
}
FLBuilder._closePanel();
FLBuilder._showLightbox();
FLBuilder._setLightboxContent( template( config ) );
} else {
config.lightbox.setContent( template( config ) );
}
return true;
},
/**
* Initializes a form when rendering is complete.
*
* @since 2.0
* @method renderComplete
* @param {Object} config
* @param {Function} callback
*/
renderComplete: function( config, callback ) {
var form = $( '.fl-builder-settings:visible' );
// This is done on a timeout to keep it from delaying painting
// of the settings form in the DOM by a fraction of a second.
setTimeout( function() {
if ( config.legacy ) {
this.renderLegacySettingsComplete( config.legacy );
}
callback();
FLBuilder._initSettingsForms();
if ( config.rules ) {
FLBuilder._initSettingsValidation( config.rules );
}
if ( config.preview ) {
FLBuilder.preview = new FLBuilderPreview( config.preview );
}
if ( config.helper ) {
config.helper.init();
}
// Cache the original settings.
if ( ! form.closest( '.fl-lightbox-wrap[data-parent]' ).length ) {
this.settings = FLBuilder._getSettingsForChangedCheck( this.config.nodeId, form );
}
}.bind( this ), 1 );
},
/**
* Renders the fields for a section in a settings form.
*
* @since 2.0
* @method renderFields
* @param {Object} fields
* @param {Object} settings
* @return {String}
*/
renderFields: function( fields, settings ) {
var template = wp.template( 'fl-builder-settings-row' ),
html = '',
field = null,
name = null,
value = null,
isMultiple = false,
responsive = null,
responsiveFields = [ 'dimension', 'unit' ],
settings = ! settings ? this.config.settings : settings,
globalSettings = FLBuilderConfig.global;
for ( name in fields ) {
field = fields[ name ];
isMultiple = field.multiple ? true : false;
supportsResponsive = $.inArray( field['type'], responsiveFields ) > -1,
value = ! _.isUndefined( settings[ name ] ) ? settings[ name ] : '';
// Use a default value if not set in the settings.
if ( _.isUndefined( settings[ name ] ) && field['default'] ) {
value = field['default'];
}
// Check to see if responsive is enabled for this field.
if ( field['responsive'] && globalSettings.responsive_enabled && ! isMultiple && supportsResponsive ) {
responsive = field['responsive'];
} else {
responsive = null;
}
html += template( {
field : field,
name : name,
rootName : name,
value : value,
preview : JSON.stringify( field['preview'] ? field['preview'] : { type: 'refresh' } ),
responsive : responsive,
rowClass : field['row_class'] ? ' ' + field['row_class'] : '',
isMultiple : isMultiple,
supportsMultiple : 'editor' !== field.type && 'photo' !== field.type && 'service' !== field.type,
settings : settings,
globalSettings : globalSettings,
template : $( '#tmpl-fl-builder-field-' + field.type )
} );
}
return html;
},
/**
* Renders a single field for a settings form.
*
* @since 2.0
* @method renderField
* @param {Object} config
* @return {String}
*/
renderField: function( config ) {
var template = wp.template( 'fl-builder-field' );
return template( config );
},
/**
* Renders a custom template for a section.
*
* @since 2.0
* @method renderSectionTemplate
* @param {Object} section
* @param {Object} settings
* @return {String}
*/
renderSectionTemplate: function( section, settings ) {
var template = wp.template( section.template.id );
return template( {
section : section,
settings : settings
} );
},
/**
* Renders a custom template for a tab.
*
* @since 2.0
* @method renderTabTemplate
* @param {Object} tab
* @param {Object} settings
* @return {String}
*/
renderTabTemplate: function( tab, settings ) {
var template = wp.template( tab.template.id );
return template( {
tab : tab,
settings : settings
} );
},
/**
* Renders any legacy custom fields that need to be
* rendered on the server with PHP.
*
* @since 2.0
* @method renderLegacyField
* @param {Object} config
* @param {Function} callback
* @return {Boolean}
*/
renderLegacySettings: function( config, callback ) {
var form = $( '.fl-builder-settings:visible' ),
name = null,
ele = null,
render = false,
data = {
'tabs' : [],
'sections' : [],
'fields' : [],
'settings' : null,
'node_id' : null
};
// Fields
form.find( '.fl-legacy-field' ).each( function() {
ele = $( this );
data.fields.push( ele.attr( 'data-field' ) );
FLBuilderSettingsForms.showFieldLoader( ele );
render = true;
} );
// Sections
form.find( '.fl-legacy-settings-section' ).each( function() {
ele = $( this );
data.sections.push( { tab: ele.attr( 'data-tab' ), section: ele.attr( 'data-section' ) } );
render = true;
} );
// Tabs
form.find( '.fl-legacy-settings-tab' ).each( function() {
ele = $( this );
data.tabs.push( ele.attr( 'data-tab' ) );
render = true;
} );
// Send a node ID if we have it, otherwise, send the settings.
if ( form.attr( 'data-node' ) ) {
data.node_id = form.attr( 'data-node' );
} else {
data.settings = FLBuilder._getOriginalSettings( form, true );
}
// Cancel an existing legacy AJAX request if we have one.
if ( this.legacyXhr ) {
this.legacyXhr.abort();
this.legacyXhr = null;
}
// We still fire the AJAX request even if we don't need to render new
// tabs, sections or fields just in case any field extras need to render.
this.legacyXhr = FLBuilder.ajax( $.extend( this.getLegacyVars(), {
action : 'render_legacy_settings',
data : data,
form : form.attr( 'data-form-id' ),
group : form.attr( 'data-form-group' ),
lightbox : form.closest( '.fl-builder-lightbox' ).attr( 'data-instance-id' )
} ), function( response ) {
FLBuilderSettingsForms.renderLegacySettingsComplete( response );
if ( render ) {
FLBuilderSettingsForms.renderComplete( config, callback );
}
FLBuilderSettingsForms.hideLightboxLoader();
} );
return render;
},
/**
* Callback for when legacy settings are done rendering.
*
* @since 2.0
* @method renderLegacySettingsComplete
* @param {String} response
*/
renderLegacySettingsComplete: function( response ) {
var data = 'object' === typeof response ? response : JSON.parse( response ),
lightbox = null,
form = null,
name = '',
field = null,
section = null,
tab = null,
settings = null;
// Get the form object.
if ( data.lightbox ) {
lightbox = $( '.fl-builder-lightbox[data-instance-id=' + data.lightbox + ']' );
form = lightbox.length ? lightbox.find( '.fl-builder-settings' ) : null;
} else {
form = $( '.fl-builder-settings:visible' );
lightbox = form.closest( '.fl-builder-lightbox' );
}
// Bail if the form no longer exists.
if ( ! form || ! form.length ) {
return;
}
// Fields
for ( name in data.fields ) {
field = $( '#fl-field-' + name ).attr( 'id', '' );
field.after( data.fields[ name ] ).remove();
}
// Field extras
for ( name in data.extras ) {
field = $( '#fl-field-' + name ).find( '.fl-field-control-wrapper' );
if ( data.extras[ name ].multiple ) {
field.each( function( i, field_item ) {
if ( ( i in data.extras[ name ].before ) && ( data.extras[ name ].before[ i ] != "" ) ) {
$( this ).prepend(
'
' +
data.extras[ name ].before[ i ] +
'
'
);
}
if ( ( i in data.extras[ name ].after ) && ( data.extras[ name ].after[ i ] != "" ) ) {
$( this ).append(
'' +
data.extras[name].after[ i ] +
'
'
);
}
});
} else {
if ( data.extras[ name ].before != "" ) {
field.prepend(
'' +
data.extras[name].before +
'
'
);
}
if ( data.extras[ name ].after != "" ) {
field.append(
'' +
data.extras[name].after +
'
'
);
}
}
}
// Sections
for ( tab in data.sections ) {
for ( name in data.sections[ tab ] ) {
section = $( '#fl-builder-settings-section-' + name );
section.html( data.sections[ tab ][ name ] );
}
}
// Tabs
for ( name in data.tabs ) {
tab = $( '#fl-builder-settings-tab-' + name );
tab.html( data.tabs[ name ] );
}
// Refresh cached settings only if it's the main form.
if ( ! lightbox.data( 'parent' ) ) {
this.settings = FLBuilder._getSettingsForChangedCheck( this.config.nodeId, form );
if ( FLBuilder.preview ) {
FLBuilder.preview._savedSettings = this.settings;
}
}
// Support for Themer before it supported JS fields. This can be removed in a future version.
if ( ! _.isUndefined( window.FLThemeBuilderFieldConnections ) ) {
FLThemeBuilderFieldConnections._initSettingsForms();
}
// Clear the legacy AJAX object.
this.legacyXhr = null;
},
/**
* Returns legacy variables that were sent in AJAX requests
* when a nested settings form was rendered.
*
* @since 2.0
* @method getLegacyVars
* @return {Object}
*/
getLegacyVars: function() {
var form = $( '.fl-builder-settings:visible' ),
lightbox = form.closest( '.fl-builder-lightbox' ),
parent = lightbox.attr( 'data-parent' ),
settings = null,
nodeId = null,
vars = {};
if ( parent ) {
parent = $( '.fl-builder-lightbox[data-instance-id=' + parent + ']' );
form = parent.find( 'form.fl-builder-settings' );
settings = FLBuilder._getSettings( form );
nodeId = form.attr( 'data-node' );
if ( nodeId ) {
vars.node_id = nodeId;
vars.node_settings = settings;
}
}
return vars;
},
/**
* Checks to see if the main form settings has changed.
*
* @since 2.0
* @method settingsHaveChanged
* @return {Boolean}
*/
settingsHaveChanged: function()
{
var form = FLBuilder._lightbox._node.find( 'form.fl-builder-settings' ),
settings = FLBuilder._getSettings( form ),
result = ! this.settings ? false : JSON.stringify( this.settings ) != JSON.stringify( settings );
return result;
},
/**
* Closes the settings lightbox when an associated node is deleted.
*
* @since 2.0
* @method closeOnDeleteNode
* @param {Object} e
* @param {String} nodeId
*/
closeOnDeleteNode: function( e, nodeId )
{
var settings = $( '.fl-builder-settings[data-node]' ),
selector = FLBuilder._contentClass + ' .fl-node-' + settings.data( 'node' );
if ( settings.length && ! $( selector ).length ) {
FLLightbox.closeAll();
}
},
/**
* Shows the loader for the current lightbox that is visible.
*
* @since 2.0
* @method showLightboxLoader
*/
showLightboxLoader: function() {
$( '.fl-builder-settings:visible' ).append( '' );
},
/**
* Hides the loader for the current lightbox that is visible.
*
* @since 2.0
* @method hideLightboxLoader
*/
hideLightboxLoader: function( ele ) {
$( '.fl-builder-settings:visible .fl-builder-loading' ).remove();
},
/**
* Shows the loader for a field that is loading.
*
* @since 2.0
* @method showFieldLoader
* @param {Object} ele
*/
showFieldLoader: function( ele ) {
var wrapper = ele.closest( '.fl-field-control' ).find( '.fl-field-control-wrapper' );
wrapper.hide().after( '' + FLBuilderStrings.fieldLoading + '
' );
},
/**
* Hides the loader for a field that is loading.
*
* @since 2.0
* @method hideFieldLoader
* @param {Object} ele
*/
hideFieldLoader: function( ele ) {
var field = ele.closest( '.fl-field' ),
wrapper = ele.closest( '.fl-field-control' ).find( '.fl-field-control-wrapper' );
wrapper.show();
field.find( '.fl-field-loader' ).remove();
}
};
/**
* Helper for working with settings forms config.
*
* @since 2.0
* @class FLBuilderSettingsConfig
*/
FLBuilderSettingsConfig = 'undefined' === typeof FLBuilderSettingsConfig ? {} : FLBuilderSettingsConfig;
$.extend( FLBuilderSettingsConfig, {
/**
* @since 2.0
* @method init
*/
init: function() {
// Save settings
FLBuilder.addHook( 'didSaveNodeSettings', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didSaveNodeSettingsComplete', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didSaveGlobalSettingsComplete', this.updateOnSaveGlobalSettings.bind( this ) );
FLBuilder.addHook( 'didSaveLayoutSettingsComplete', this.updateOnSaveLayoutSettings.bind( this ) );
// Add nodes
FLBuilder.addHook( 'didAddRow', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didAddColumnGroup', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didAddColumn', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didAddModule', this.updateOnNodeEvent.bind( this ) );
// Delete nodes
FLBuilder.addHook( 'didDeleteRow', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didDeleteColumn', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didDeleteModule', this.updateOnNodeEvent.bind( this ) );
// Duplicate nodes
FLBuilder.addHook( 'didDuplicateRow', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didDuplicateColumn', this.updateOnNodeEvent.bind( this ) );
FLBuilder.addHook( 'didDuplicateModule', this.updateOnNodeEvent.bind( this ) );
// Resize nodes
FLBuilder.addHook( 'didResizeRow', this.updateOnRowResize.bind( this ) );
FLBuilder.addHook( 'didResizeColumn', this.updateOnColumnResize.bind( this ) );
// Reset node widths
FLBuilder.addHook( 'didResetRowWidth', this.updateOnResetRowWidth.bind( this ) );
FLBuilder.addHook( 'didResetColumnWidths', this.updateOnResetColumnWidths.bind( this ) );
// Apply templates
FLBuilder.addHook( 'didApplyTemplateComplete', this.updateOnApplyTemplate.bind( this ) );
FLBuilder.addHook( 'didApplyRowTemplateComplete', this.updateOnApplyTemplate.bind( this ) );
FLBuilder.addHook( 'didApplyColTemplateComplete', this.updateOnApplyTemplate.bind( this ) );
FLBuilder.addHook( 'didSaveGlobalNodeTemplate', this.updateOnApplyTemplate.bind( this ) );
FLBuilder.addHook( 'didRestoreRevisionComplete', this.updateOnApplyTemplate.bind( this ) );
},
/**
* Updates the global settings when they are saved.
*
* @since 2.0
* @method updateOnSaveGlobalSettings
* @param {Object} e
* @param {Object} settings
*/
updateOnSaveGlobalSettings: function( e, settings ) {
this.settings.global = settings;
},
/**
* Updates the layout settings when they are saved.
*
* @since 2.0
* @method updateOnSaveLayoutSettings
* @param {Object} e
* @param {Object} settings
*/
updateOnSaveLayoutSettings: function( e, settings ) {
this.settings.layout = settings;
},
/**
* Updates the node config when an event is triggered.
*
* @since 2.0
* @method updateOnNodeEvent
*/
updateOnNodeEvent: function() {
var event = arguments[0];
if ( event.namespace.indexOf( 'didAdd' ) > -1 ) {
this.addNode( arguments[1] );
} else if ( event.namespace.indexOf( 'didSaveNodeSettings' ) > -1 ) {
this.updateNode( arguments[1].nodeId, arguments[1].settings );
} else if ( event.namespace.indexOf( 'didDelete' ) > -1 ) {
this.deleteNodes();
} else if ( event.namespace.indexOf( 'didDuplicate' ) > -1 ) {
this.duplicateNode( arguments[1].oldNodeId, arguments[1].newNodeId );
}
},
/**
* Updates the node config when a row is resized.
*
* @since 2.0
* @method updateOnRowResize
* @param {Object} e
* @param {Object} data
*/
updateOnRowResize: function( e, data ) {
this.nodes[ data.rowId ].max_content_width = data.rowWidth;
},
/**
* Updates the node config when a row width is reset.
*
* @since 2.0
* @method updateOnResetRowWidth
* @param {Object} e
* @param {String} nodeId
*/
updateOnResetRowWidth: function( e, nodeId ) {
this.nodes[ nodeId ].max_content_width = '';
},
/**
* Updates the node config when a column is resized.
*
* @since 2.0
* @method updateOnColumnResize
* @param {Object} e
* @param {Object} data
*/
updateOnColumnResize: function( e, data ) {
this.nodes[ data.colId ].size = data.colWidth;
this.nodes[ data.siblingId ].size = data.siblingWidth;
},
/**
* Updates the node config when column widths are reset.
*
* @since 2.0
* @method updateOnResetColumnWidths
* @param {Object} e
* @param {Object} data
*/
updateOnResetColumnWidths: function( e, data ) {
var self = this;
data.cols.each( function() {
var col = $( this ),
colId = col.attr( 'data-node' );
if ( self.nodes[ colId ] ) {
self.nodes[ colId ].size = parseFloat( col[0].style.width );
}
} );
},
/**
* Updates the node config when a template is applied.
*
* @since 2.0
* @method updateOnApplyTemplate
* @param {Object} e
* @param {Object} config
*/
updateOnApplyTemplate: function( e, config ) {
this.nodes = config.nodes;
this.attachments = config.attachments;
},
/**
* Adds the settings config for a new node.
*
* @since 2.0
* @method addNode
* @param {String} nodeId
* @param {Object} settings
*/
addNode: function( nodeId, settings ) {
var node = $( '.fl-node-' + nodeId ),
isRow = node.hasClass( 'fl-row' ),
isCol = node.hasClass( 'fl-col' ),
isColGroup = node.hasClass( 'fl-col-group' ),
isModule = node.hasClass( 'fl-module' ),
self = this;
if ( this.nodes[ nodeId ] ) {
return;
}
if ( ! settings ) {
if ( isRow ) {
settings = $.extend( {}, this.defaults.row );
} else if ( isCol ) {
settings = $.extend( {}, this.defaults.column );
} else if ( isModule ) {
settings = $.extend( {}, this.defaults.modules[ node.attr( 'data-type' ) ] );
}
if ( isRow || isColGroup ) {
node.find( '.fl-col' ).each( function() {
var col = $( this ), defaults = $.extend( {}, self.defaults.column );
defaults.size = parseFloat( col[0].style.width );
self.addNode( col.attr( 'data-node' ), defaults );
} );
} else if ( isModule ) {
self.addNode( node.closest( '.fl-row' ).attr( 'data-node' ) );
self.addNode( node.closest( '.fl-col' ).attr( 'data-node' ) );
self.updateOnResetColumnWidths( null, {
cols: node.closest( '.fl-col-group' ).find( '> .fl-col' )
} );
}
}
if ( settings ) {
this.nodes[ nodeId ] = settings;
}
},
/**
* Update the settings config for a node.
*
* @since 2.0
* @method updateNode
* @param {String} nodeId
* @param {Object} settings
*/
updateNode: function( nodeId, settings ) {
var node = $( '.fl-node-' + nodeId ),
self = this;
if ( node.hasClass( 'fl-col' ) ) {
node.closest( '.fl-col-group' ).find( '> .fl-col' ).each( function() {
var col = $( this ), colId = col.attr( 'data-node' );
self.nodes[ colId ].size = parseFloat( col[0].style.width );
self.nodes[ colId ].equal_height = settings.equal_height;
self.nodes[ colId ].content_alignment = settings.content_alignment;
self.nodes[ colId ].responsive_order = settings.responsive_order;
} );
}
this.nodes[ nodeId ] = settings;
},
/**
* Duplicates settings config for a node.
*
* @since 2.0
* @method duplicateNode
* @param {String} oldNode
* @param {String} newNode
*/
duplicateNode: function( oldNodeId, newNodeId ) {
var newNode = $( '.fl-node-' + newNodeId ),
newNodes = newNode.find( '[data-node]' ),
oldNode = $( '.fl-node-' + oldNodeId ),
oldNodes = oldNode.find( '[data-node]' ),
self = this;
this.nodes[ newNodeId ] = this.nodes[ oldNodeId ];
newNodes.each( function( i ) {
oldNodeId = oldNodes.eq( i ).attr( 'data-node' );
newNodeId = $( this ).attr( 'data-node' );
if ( self.nodes[ oldNodeId ] ) {
self.nodes[ newNodeId ] = self.nodes[ oldNodeId ];
}
} );
},
/**
* Deletes any nodes that are no longer in the DOM.
*
* @since 2.0
* @method deleteNodes
*/
deleteNodes: function() {
var nodeId = '',
content = $( FLBuilder._contentClass ).html();
for ( nodeId in this.nodes ) {
if ( content.indexOf( nodeId ) === -1 ) {
this.nodes[ nodeId ] = null;
delete this.nodes[ nodeId ];
}
}
}
} );
$( function() {
FLBuilderSettingsConfig.init();
FLBuilderSettingsForms.init();
} );
} )( jQuery );