(function($){ /** * Helper class for dealing with live previews. * * @class FLBuilderPreview * @since 1.3.3 * @param {Object} config */ FLBuilderPreview = function( config ) { // Set the type. this.type = config.type; // Save the current state. this._saveState(); // Initialize the preview. if ( config.layout ) { FLBuilder._renderLayout( config.layout, function() { this._init(); if ( config.callback ) { config.callback(); } }.bind( this ) ); } else { this._init(); } }; /** * Stores all the fonts and weights of all font fields. * This is used to render the stylesheet with Google Fonts. * * @since 1.6.3 * @access private * @property {Array} _fontsList */ FLBuilderPreview._fontsList = {}; /** * Returns a formatted selector string for a preview. * * @since 2.1 * @method getFormattedSelector * @param {String} selector A CSS selector string. * @return {String} */ FLBuilderPreview.getFormattedSelector = function( prefix, selector ) { var formatted = '', parts = selector.split( ',' ), i = 0; for ( ; i < parts.length; i++ ) { if ( parts[ i ].indexOf( '{node}' ) > -1 ) { formatted = parts[ i ].replace( '{node}', prefix ); } else { formatted += prefix + ' ' + parts[ i ]; } if ( i != parts.length - 1 ) { formatted += ', '; } } return formatted; }; /** * Prototype for new instances. * * @since 1.3.3 * @property {Object} prototype */ FLBuilderPreview.prototype = { /** * The type of node that we are previewing. * * @since 1.3.3 * @property {String} type */ type : '', /** * The ID of node that we are previewing. * * @since 1.3.3 * @property {String} nodeId */ nodeId : null, /** * An object with data for each CSS class * in the preview. * * @since 1.3.3 * @property {Object} classes */ classes : {}, /** * An object with references to each element * in the preview. * * @since 1.3.3 * @property {Object} elements */ elements : {}, /** * An object that contains data for the current * state of a layout before changes are made. * * @since 1.3.3 * @property {Object} state */ state : null, /** * Node settings saved when the preview was initalized. * * @since 1.7 * @access private * @property {Object} _savedSettings */ _savedSettings : null, /** * An instance of FLStyleSheet for the current preview. * * @since 1.3.3 * @access private * @property {FLStyleSheet} _styleSheet */ _styleSheet : null, /** * An instance of FLStyleSheet for the medium device preview. * * @since 1.9 * @access private * @property {FLStyleSheet} _styleSheetMedium */ _styleSheetMedium : null, /** * An instance of FLStyleSheet for the responsive device preview. * * @since 1.9 * @access private * @property {FLStyleSheet} _styleSheet */ _styleSheetResponsive : null, /** * A timeout object for delaying the current preview refresh. * * @since 1.3.3 * @access private * @property {Object} _timeout */ _timeout : null, /** * A timeout object for delaying when we show the loading * graphic for refresh previews. * * @since 1.10 * @access private * @property {Object} _loaderTimeout */ _loaderTimeout : null, /** * Stores the last classname for a classname preview. * * @since 1.3.3 * @access private * @property {String} _lastClassName */ _lastClassName : null, /** * A reference to the AJAX object for a preview refresh. * * @since 1.3.3 * @access private * @property {Object} _xhr */ _xhr : null, /** * Initializes a builder preview. * * @since 1.3.3 * @access private * @method _init */ _init: function() { // Node Id this.nodeId = $('.fl-builder-settings').data('node'); // Save settings this._saveSettings(); // Elements and Class Names this._initElementsAndClasses(); // Create the preview stylesheets this._createSheets(); // Responsive previews this._initResponsivePreviews(); // Default field previews this._initDefaultFieldPreviews(); // Init switch(this.type) { case 'row': this._initRow(); break; case 'col': this._initColumn(); break; case 'module': this._initModule(); break; } }, /** * Saves the current settings to be checked to see if * anything has changed when a preview is canceled. * * @since 1.7 * @access private * @method _saveSettings */ _saveSettings: function() { var form = $('.fl-builder-settings-lightbox .fl-builder-settings'); this._savedSettings = FLBuilder._getSettingsForChangedCheck( this.nodeId, form ); }, /** * Checks to see if the settings have changed. * * @since 1.7 * @access private * @method _settingsHaveChanged * @return bool */ _settingsHaveChanged: function() { var form = $('.fl-builder-settings-lightbox .fl-builder-settings'), settings = FLBuilder._getSettings( form ); return JSON.stringify( this._savedSettings ) != JSON.stringify( settings ); }, /** * Initializes the classname and element references * for this preview. * * @since 1.3.3 * @access private * @method _initElementsAndClasses */ _initElementsAndClasses: function() { var contentClass; // Content Class if(this.type == 'row') { contentClass = '.fl-row-content-wrap'; } else { contentClass = '.fl-' + this.type + '-content'; } // Class Names $.extend(this.classes, { settings : '.fl-builder-' + this.type + '-settings', settingsHeader : '.fl-builder-' + this.type + '-settings .fl-lightbox-header', node : FLBuilder._contentClass + ' .fl-node-' + this.nodeId, content : FLBuilder._contentClass + ' .fl-node-' + this.nodeId + ' > ' + contentClass }); // Elements $.extend(this.elements, { settings : $(this.classes.settings), settingsHeader : $(this.classes.settingsHeader), node : $(this.classes.node), content : $(this.classes.content) }); }, /** * Creates the stylesheets for default, medium * and responsive previews. * * @since 1.9 * @method _createSheets */ _createSheets: function() { this._destroySheets(); if ( ! this._styleSheet ) { this._styleSheet = new FLStyleSheet( { id : 'fl-builder-preview', className : 'fl-builder-preview-style' } ); } if ( ! this._styleSheetMedium ) { this._styleSheetMedium = new FLStyleSheet( { id : 'fl-builder-preview-medium', className : 'fl-builder-preview-style' } ); } if ( ! this._styleSheetResponsive ) { this._styleSheetResponsive = new FLStyleSheet( { id : 'fl-builder-preview-responsive', className : 'fl-builder-preview-style' } ); } }, /** * Destroys all preview sheets. * * @since 1.9 * @method _destroySheets */ _destroySheets: function() { if ( this._styleSheet ) { this._styleSheet.destroy(); this._styleSheet = null; } if ( this._styleSheetMedium ) { this._styleSheetMedium.destroy(); this._styleSheetMedium = null; } if ( this._styleSheetResponsive ) { this._styleSheetResponsive.destroy(); this._styleSheetResponsive = null; } }, /** * Updates a CSS rule for this preview. * * @since 1.3.3 * @method updateCSSRule * @param {String} selector The CSS selector to update. * @param {String} property The CSS property to update. * @param {String} value The CSS value to update. */ updateCSSRule: function( selector, property, value ) { this._styleSheet.updateRule( selector, property, value ); }, /** * Runs a delay with a callback. * * @since 1.3.3 * @method delay * @param {Number} length How long to wait before running the callback. * @param {Function} callback A function to call when the delay is complete. */ delay: function(length, callback) { this._cancelDelay(); this._timeout = setTimeout(callback, length); }, /** * Cancels a preview refresh delay. * * @since 1.3.3 * @access private * @method _cancelDelay */ _cancelDelay: function() { if(this._timeout !== null) { clearTimeout(this._timeout); } }, /** * Converts a hex value to an array of RGB values. * * @since 1.3.3 * @method hexToRgb * @param {String} hex * @return {Array} */ hexToRgb: function(hex) { var bigInt = parseInt(hex, 16), r = (bigInt >> 16) & 255, g = (bigInt >> 8) & 255, b = bigInt & 255; return [r, g, b]; }, /** * Parses a float or returns 0 if we don't have a number. * * @since 1.3.3 * @method parseFloat * @param {Number} value * @return {Number} */ parseFloat: function(value) { return isNaN(parseFloat(value)) ? 0 : parseFloat(value); }, /* Responsive Previews ----------------------------------------------------------*/ /** * Initializes logic for responsive previews. * * @since 1.9 * @method _initResponsivePreviews */ _initResponsivePreviews: function() { FLBuilder.addHook( 'responsive-editing-switched.preview', $.proxy( this._responsiveEditingSwitched, this ) ); }, /** * Destroys responsive preview events. * * @since 1.9 * @method _destroyResponsivePreviews */ _destroyResponsivePreviews: function() { FLBuilder.removeHook( 'responsive-editing-switched.preview' ); }, /** * Initializes logic for responsive previews. * * @since 1.9 * @method _responsiveEditingSwitched */ _responsiveEditingSwitched: function( e, mode ) { if ( 'default' == mode ) { this._styleSheetMedium.disable(); this._styleSheetResponsive.disable(); } else if ( 'medium' == mode ) { this._styleSheetMedium.enable(); this._styleSheetResponsive.disable(); } else if ( 'responsive' == mode ) { this._styleSheetMedium.disable(); this._styleSheetResponsive.enable(); } }, /** * Updates a CSS rule for responsive preview. * * @since 1.9 * @method updateResponsiveCSSRule * @param {String} selector The CSS selector to update. * @param {String} property The CSS property to update. * @param {String} value The CSS value to update. */ updateResponsiveCSSRule: function( selector, property, value ) { var mode = FLBuilderResponsiveEditing._mode, sheetKey = 'default' == mode ? '' : mode.charAt(0).toUpperCase() + mode.slice(1); this[ '_styleSheet' + sheetKey ].updateRule( selector, property, value ); }, /* States ----------------------------------------------------------*/ /** * Saves the current state of a layout. * * @since 1.3.3 * @access private * @method _saveState */ _saveState: function() { var post = FLBuilderConfig.postId, css = $('link[href*="/cache/' + post + '"]').attr('href'), js = $('script[src*="/cache/' + post + '"]').attr('src'), html = $(FLBuilder._contentClass).html(); this.state = { css : css, js : js, html : html }; }, /** * Runs a preview refresh for the current settings lightbox. * * @since 1.3.3 * @method preview */ preview: function() { var form = $('.fl-builder-settings-lightbox .fl-builder-settings'), nodeId = form.attr('data-node'), settings = FLBuilder._getSettings(form); // Show the node as loading. FLBuilder._showNodeLoading( nodeId ); // Abort an existing preview request. this._cancelPreview(); // Make a new preview request. this._xhr = FLBuilder.ajax({ action : 'render_layout', node_id : nodeId, node_preview : settings }, $.proxy(this._renderPreview, this)); }, /** * Runs a preview refresh with a delay. * * @since 1.3.3 * @method delayPreview */ delayPreview: function(e) { var heading = typeof e == 'undefined' ? [] : $(e.target).closest('tr').find('th'), widgetHeading = $('.fl-builder-widget-settings .fl-builder-settings-title'), lightboxHeading = $('.fl-builder-settings .fl-lightbox-header'), loaderSrc = FLBuilderLayoutConfig.paths.pluginUrl + 'img/ajax-loader-small.svg', loader = $(''); this.delay(1000, $.proxy(this.preview, this)); this._loaderTimeout = setTimeout( function() { $('.fl-builder-preview-loader').remove(); if(heading.length > 0) { heading.append(loader); } else if(widgetHeading.length > 0) { widgetHeading.append(loader); } else if(lightboxHeading.length > 0) { lightboxHeading.append(loader); } }, 1500 ); }, /** * Cancels a preview refresh. * * @since 1.3.3 * @access private * @method _cancelPreview */ _cancelPreview: function() { if(this._xhr) { this._xhr.abort(); this._xhr = null; } }, /** * Renders the response of a preview refresh. * * @since 1.3.3 * @access private * @method _renderPreview * @param {String} response The JSON encoded response. */ _renderPreview: function(response) { this._xhr = null; FLBuilder._renderLayout(response, $.proxy(this._renderPreviewComplete, this)); }, /** * Fires when a preview refresh has finished rendering. * * @since 1.3.3 * @access private * @method _renderPreviewComplete */ _renderPreviewComplete: function() { // Refresh the preview styles. this._createSheets(); // Refresh the elements. this._initElementsAndClasses(); // Clear the loader timeout. if(this._loaderTimeout !== null) { clearTimeout(this._loaderTimeout); } // Remove the loading graphic. $('.fl-builder-preview-loader').remove(); // Fire the preview rendered event. $( FLBuilder._contentClass ).trigger( 'fl-builder.preview-rendered' ); }, /** * Reverts a preview to the state that was saved * before the preview was initialized. * * @since 1.3.3 * @method revert */ revert: function() { if ( ! this._settingsHaveChanged() ) { this.clear(); return; } FLBuilder._updateNode( this.nodeId, function() { this.clear(); }.bind( this ) ); }, /** * Cancels a preview refresh. * * @since 1.3.3 * @method clear */ cancel: function() { this._cancelDelay(); this._cancelPreview(); }, /** * Cancels a preview refresh and removes * any stylesheet changes. * * @since 1.3.3 * @method clear */ clear: function() { // Canel any preview delays or requests. this.cancel(); // Destroy the preview stylesheet. this._destroySheets(); // Destroy responsive editing previews. this._destroyResponsivePreviews(); }, /* Node Text Color Settings ----------------------------------------------------------*/ /** * Initializes node text color previews. * * @since 1.3.3 * @access private * @method _initNodeTextColor */ _initNodeTextColor: function() { // Elements $.extend(this.elements, { textColor : $(this.classes.settings + ' input[name=text_color]'), linkColor : $(this.classes.settings + ' input[name=link_color]'), hoverColor : $(this.classes.settings + ' input[name=hover_color]'), headingColor : $(this.classes.settings + ' input[name=heading_color]') }); // Events this.elements.textColor.on('change', $.proxy(this._textColorChange, this)); this.elements.linkColor.on('change', $.proxy(this._textColorChange, this)); this.elements.hoverColor.on('change', $.proxy(this._textColorChange, this)); this.elements.headingColor.on('change', $.proxy(this._textColorChange, this)); }, /** * Fires when the text color field for a node * is changed. * * @since 1.3.3 * @access private * @method _textColorChange * @param {Object} e An event object. */ _textColorChange: function(e) { var textColor = this.elements.textColor.val(), linkColor = this.elements.linkColor.val(), hoverColor = this.elements.hoverColor.val(), headingColor = this.elements.headingColor.val(); linkColor = linkColor === '' ? textColor : linkColor; hoverColor = hoverColor === '' ? textColor : hoverColor; headingColor = headingColor === '' ? textColor : headingColor; this.delay(100, $.proxy(function(){ // Update Text color. if(textColor === '') { this.updateCSSRule(this.classes.node, 'color', 'inherit'); } else { this.updateCSSRule(this.classes.node, 'color', '#' + textColor); } // Update Link Color if ( linkColor === '' ) { this.updateCSSRule(this.classes.node + ' a', 'color', 'inherit'); } else { this.updateCSSRule(this.classes.node + ' a', 'color', '#' + linkColor); } // Hover Color if(hoverColor === '') { this.updateCSSRule(this.classes.node + ' a:hover', 'color', 'inherit'); } else { this.updateCSSRule(this.classes.node + ' a:hover', 'color', '#' + hoverColor); } // Heading Color if(headingColor === '') { this.updateCSSRule(this.classes.node + ' h1', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h2', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h3', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h4', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h5', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h6', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h1 a', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h2 a', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h3 a', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h4 a', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h5 a', 'color', 'inherit'); this.updateCSSRule(this.classes.node + ' h6 a', 'color', 'inherit'); } else { this.updateCSSRule(this.classes.node + ' h1', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h2', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h3', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h4', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h5', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h6', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h1 a', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h2 a', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h3 a', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h4 a', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h5 a', 'color', '#' + headingColor); this.updateCSSRule(this.classes.node + ' h6 a', 'color', '#' + headingColor); } }, this)); }, /* Node Bg Settings ----------------------------------------------------------*/ /** * Initializes node background previews. * * @since 1.3.3 * @access private * @method _initNodeBg */ _initNodeBg: function() { // Elements $.extend(this.elements, { bgType : $(this.classes.settings + ' select[name=bg_type]'), bgColor : $(this.classes.settings + ' input[name=bg_color]'), bgColorPicker : $(this.classes.settings + ' .fl-picker-bg_color'), bgOpacity : $(this.classes.settings + ' input[name=bg_opacity]'), bgImageSrc : $(this.classes.settings + ' select[name=bg_image_src]'), bgRepeat : $(this.classes.settings + ' select[name=bg_repeat]'), bgPosition : $(this.classes.settings + ' select[name=bg_position]'), bgAttachment : $(this.classes.settings + ' select[name=bg_attachment]'), bgSize : $(this.classes.settings + ' select[name=bg_size]'), bgVideoSource : $(this.classes.settings + ' select[name=bg_video_source]'), bgVideo : $(this.classes.settings + ' input[name=bg_video]'), bgVideoServiceUrl : $(this.classes.settings + ' input[name=bg_video_service_url]'), bgVideoFallbackSrc : $(this.classes.settings + ' select[name=bg_video_fallback_src]'), bgSlideshowSource : $(this.classes.settings + ' select[name=ss_source]'), bgSlideshowPhotos : $(this.classes.settings + ' input[name=ss_photos]'), bgSlideshowFeedUrl : $(this.classes.settings + ' input[name=ss_feed_url]'), bgSlideshowSpeed : $(this.classes.settings + ' input[name=ss_speed]'), bgSlideshowTrans : $(this.classes.settings + ' select[name=ss_transition]'), bgSlideshowTransSpeed : $(this.classes.settings + ' input[name=ss_transitionDuration]'), bgParallaxImageSrc : $(this.classes.settings + ' select[name=bg_parallax_image_src]'), bgOverlayColor : $(this.classes.settings + ' input[name=bg_overlay_color]'), bgOverlayOpacity : $(this.classes.settings + ' input[name=bg_overlay_opacity]') }); // Events this.elements.bgType.on( 'change', $.proxy(this._bgTypeChange, this)); this.elements.bgColor.on( 'change', $.proxy(this._bgColorChange, this)); this.elements.bgOpacity.on( 'keyup', $.proxy(this._bgOpacityChange, this)); this.elements.bgImageSrc.on( 'change', $.proxy(this._bgPhotoChange, this)); this.elements.bgRepeat.on( 'change', $.proxy(this._bgPhotoChange, this)); this.elements.bgPosition.on( 'change', $.proxy(this._bgPhotoChange, this)); this.elements.bgAttachment.on( 'change', $.proxy(this._bgPhotoChange, this)); this.elements.bgSize.on( 'change', $.proxy(this._bgPhotoChange, this)); this.elements.bgVideoServiceUrl.on( 'change', $.proxy(this._bgVideoChange, this)); this.elements.bgSlideshowSource.on( 'change', $.proxy(this._bgSlideshowChange, this)); this.elements.bgSlideshowPhotos.on( 'change', $.proxy(this._bgSlideshowChange, this)); this.elements.bgSlideshowFeedUrl.on( 'keyup', $.proxy(this._bgSlideshowChange, this)); this.elements.bgSlideshowSpeed.on( 'keyup', $.proxy(this._bgSlideshowChange, this)); this.elements.bgSlideshowTrans.on( 'change', $.proxy(this._bgSlideshowChange, this)); this.elements.bgSlideshowTransSpeed.on( 'keyup', $.proxy(this._bgSlideshowChange, this)); this.elements.bgParallaxImageSrc.on( 'change', $.proxy(this._bgParallaxChange, this)); this.elements.bgOverlayColor.on( 'change', $.proxy(this._bgOverlayChange, this)); this.elements.bgOverlayOpacity.on( 'keyup', $.proxy(this._bgOverlayChange, this)); }, /** * Fires when the background type field of * a node changes. * * @since 1.3.3 * @access private * @method _bgTypeChange * @param {Object} e An event object. */ _bgTypeChange: function(e) { var val = this.elements.bgType.val(); // Clear bg styles first. this.elements.node.removeClass('fl-row-bg-video'); this.elements.node.removeClass('fl-row-bg-slideshow'); this.elements.node.removeClass('fl-row-bg-parallax'); this.elements.node.find('.fl-bg-video').remove(); this.elements.node.find('.fl-bg-slideshow').remove(); this.elements.content.css('background-image', ''); this.updateCSSRule(this.classes.content, { 'background-color' : 'transparent', 'background-image' : 'none' }); // None if(val == 'none') { this._bgOverlayClear(); } // Color else if(val == 'color') { this.elements.bgColor.trigger('change'); this._bgOverlayClear(); } // Photo else if(val == 'photo') { this.elements.bgColor.trigger('change'); this.elements.bgImageSrc.trigger('change'); } // Video else if(val == 'video') { this.elements.bgColor.trigger('change'); this._bgVideoChange(); } // Slideshow else if(val == 'slideshow') { this.elements.bgColor.trigger('change'); this._bgSlideshowChange(); } // Parallax else if(val == 'parallax') { this.elements.bgColor.trigger('change'); this.elements.bgParallaxImageSrc.trigger('change'); } }, /** * Fires when the background color field of * a node changes. * * @since 1.3.3 * @access private * @method _bgColorChange * @param {Object} e An event object. */ _bgColorChange: function(e) { var rgb, alpha, value; if(this.elements.bgColor.val() === '' || isNaN(this.elements.bgOpacity.val())) { this.updateCSSRule(this.classes.content, 'background-color', 'transparent'); } else { rgb = this.hexToRgb( this.elements.bgColor.val() ); alpha = this.parseFloat(this.elements.bgOpacity.val())/100; value = 'rgba(' + rgb.join() + ', ' + alpha + ')'; this.delay(100, $.proxy(function(){ this.updateCSSRule(this.classes.content, 'background-color', value); }, this)); } }, /** * Fires when the background opacity field of * a node changes. * * @since 1.3.3 * @access private * @method _bgOpacityChange * @param {Object} e An event object. */ _bgOpacityChange: function(e) { this.elements.bgColor.trigger('change'); }, /** * Fires when the background photo field of * a node changes. * * @since 1.3.3 * @access private * @method _bgPhotoChange * @param {Object} e An event object. */ _bgPhotoChange: function(e) { if(this.elements.bgImageSrc.val()) { this.updateCSSRule(this.classes.content, { 'background-image' : 'url(' + this.elements.bgImageSrc.val() + ')', 'background-repeat' : this.elements.bgRepeat.val(), 'background-position' : this.elements.bgPosition.val(), 'background-attachment' : this.elements.bgAttachment.val(), 'background-size' : this.elements.bgSize.val() }); } else { this.updateCSSRule(this.classes.content, { 'background-image' : 'none' }); } }, /** * Fires when the background video field of * a node changes. * * @since 1.9.2 * @access private * @method _bgVideoChange * @param {Object} e An event object. */ _bgVideoChange: function(e) { var eles = this.elements, source = eles.bgVideoSource.val(), video = eles.bgVideo.val(), videoUrl = eles.bgVideoServiceUrl.val(), youtubePlayer = 'https://www.youtube.com/iframe_api', vimeoPlayer = 'https://player.vimeo.com/api/player.js', scriptTag = $( '