/** * Global window objects for the USBuilder ecosystem: * - $usb: Core mounting and extension initialization. * - $usbcore: Builder and extension helper functions. * - usGlobalData: Context data for USBuilder import. * - $usof: UpSolution Framework. * - $ush: UpSolution Helper Library. */ ! function( $, _undefined ) { const _window = window; const _document = document; const abs = Math.abs; const ceil = Math.ceil; _window.$ush = _window.$ush || {}; _window.usGlobalData = _window.usGlobalData || {}; // Direction constants. DIRECTION_TOP = 'top'; DIRECTION_BOTTOM = 'bottom'; // Regular expression for check and extract alias from usbid. const REGEXP_USBID_ALIAS = /^([\w\-]+:\d+)\|([\dA-Za-z\-]+)$/; // Regular expression for find space. const REGEXP_SPACE = /\p{Zs}/u; // Regular expression for finding builder IDs. const REGEXP_USBID = /(\s?usbid="([^\"]+)?")/g; // List of reserved builder modes. const BUILDER_MODES = [ 'unknown', // mode disables all 'edit', // editing mode 'view', // view only without editing 'drag_from_preview', // dragging element from preview 'drag_new_element', // dragging new element from left panel ]; // Modes in which a drag & drop operation is in progress (see isDragMode()). const DRAG_MODES = [ 'drag_new_element', 'drag_from_preview' ]; // Active builder mode. var _activeMode = 'view'; // Default page data. const _defaultPageData = { content: '', customCss: '', fields: {}, // page fields post_title, post_status, post_name etc. postMeta: {}, }; // Private temp data. const _tempData = { usedElementIds: [], // list of generated IDs. dragDropInitialized: false, isSaving: false, // the AJAX process of save data on the backend. savedPageData: $ush.clone( _defaultPageData ), transit: null, customTransit: null, excludedElements: {}, }; // Data structure: column:1 => { parentId: row:1, elmIndex: 0 } const elementsMap = new Map; /** * Page Builder - Builder for edit, remove and add shortcodes to a page. */ function Builder() { const self = this; // Default root container self.defaultRootContainer = 'root_container'; // Root container of the Post Content element self.postContentRootContainer = 'post_content_root_container'; // Current root container self.rootContainer = self.defaultRootContainer; // Post content with remove rows self.isRemoveRows = false; // Shortcode element ID (e.g. us_btn:1) self.selectedElmId; // Private "Variables" self._isReloadPreviewAfterSave = false; self.pageData = $ush.clone( _defaultPageData ); // When the user is trying to load another page, or reloads current page // show a confirmation dialog when there are unsaved changes _window.onbeforeunload = ( e ) => { if ( self.isPageChanged() ) { e.preventDefault(); // The return string is needed for browser compat // See https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event return $usb.getTextTranslation( 'page_leave_warning' ); } }; // Bindable events self._events = { // Local handlers dragstart: self._dragstart.bind( self ), iframeReady: self._iframeReady.bind( self ), maybeDrag: self._maybeDrag.bind( self ), maybeStartDrag: self._maybeStartDrag.bind( self ), modeChanged: self._modeChanged.bind( self ), historyChanged: self.historyChanged.bind( self ), // Event handlers from an iframe dropzoneAddRow: self.dropzoneAddRow.bind( self ), elmCopy: self._elmCopy.bind( self ), elmDelete: self._elmDelete.bind( self ), elmDuplicate: self._elmDuplicate.bind( self ), elmPaste: self._elmPaste.bind( self ), elmSelected: self.elmSelected.bind( self ), endDrag: self._endDrag.bind( self ), }; $( () => { // Elements self.$container = $( '#usb-wrapper' ); // Events $usb.$document .on( 'dragstart', self._events.dragstart ); // Get custom transit node _tempData.customTransit = document.querySelector( '.usb-custom-transit' ); } ); // Private events $usb .on( 'iframeReady', self._events.iframeReady ) .on( 'builder.modeChanged', self._events.modeChanged ) .on( 'builder.endDrag', self._events.endDrag ) .on( 'builder.elmCopy', self._events.elmCopy ) .on( 'builder.elmPaste', self._events.elmPaste ) .on( 'builder.elmDelete', self._events.elmDelete ) .on( 'builder.elmDuplicate', self._events.elmDuplicate ) .on( 'builder.elmSelected', self._events.elmSelected ) .on( 'builder.dropzoneAddRow', self._events.dropzoneAddRow ) .on( 'history.changed', self._events.historyChanged ); }; // Private Events $.extend( Builder.prototype, $ush.mixinEvents, { /** * The handler that is called every time the mode is changed * * @event handler */ _modeChanged: function() { $usb.postMessage( 'doAction', 'hideHighlight' ); }, /** * @event handler */ _iframeReady: function() { const self = this; if ( ! $usb.iframeIsReady ) { return; } const iframeWindow = $usb.iframe.contentWindow; // If meta parameters are set for preview we ignore data save if ( ( iframeWindow.location.search || '' ).indexOf( '&meta' ) !== -1 ) { return; } $usb.postMessage( 'doAction', 'hideHighlight' ); /** * @type {{}} Import data and save the current and last saved object * NOTE: The data is unrelated because the preview can be reloaded to show the changes */ self.pageData = $ush.clone( ( iframeWindow.usGlobalData || {} ).pageData || {}, _defaultPageData ); _tempData.savedPageData = $ush.clone( self.pageData ); // set first saved pageData // NOTE: A timeout is used to release the main thread. $ush.timeout( self.reloadElementsMap.bind( self ), 0 ); }, /** * Handle selected element. * * @event * @param {String} id Element usbid, e.g. "us_btn:1" */ elmSelected: function( id ) { const self = this; if ( ! self.isMode( 'edit' ) || ! self.doesElmExist( id ) ) { return; } // Set the active element in navigator $usb.navigator.setActive( id, /* expand parents */true ); if ( self.selectedElmId === id ) { return; } if ( self.doesElmExist( id ) ) { if ( $usb.panel ) { // Reset scroll after fieldset init self.one( 'panel.afterInitFieldset', () => { $usb.panel.resetBodyScroll(); } ); $usb.builderPanel.initElmFieldset( id ); } } else { $usb.postMessage( 'doAction', 'hideHighlight' ); } }, /** * Creates a duplicate of an element. * * @event handler * @param {String} id Shortcode's usbid, e.g. "us_btn:1" */ _elmDuplicate: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return; } var parentId = self.getElmParentId( id ), strShortcode = self.getElmShortcode( id ) || '', newElmId; // new spare ID strShortcode = strShortcode // Remove all `el_id` from the design_options .replace( /(\s?el_id="([^\"]+)")/gi, '' ) // Replace all ids .replace( /usbid="([^\"]+)"/gi, ( _, elmId ) => { elmId = self.getSpareElmId( elmId ); if ( ! newElmId ) { newElmId = elmId; } return `usbid="${elmId}"`; } ); if ( ! strShortcode || ! newElmId ) { return; } const siblingsIds = self.getElmSiblingsId( id ) || []; // Define index for new shortcode var index = 0; for ( ; index < siblingsIds.length; index++ ) { if ( siblingsIds[ index ] === id ) { index++; // next index break; } } // Added shortcode to content if ( ! self.insertShortcodeIntoContent( parentId, index, strShortcode ) ) { return; } self.reloadElementsMap(); // Reload element in preview if ( self.isReloadElm( parentId ) ) { self.reloadElmInPreview( parentId ); $usb.history.commitChange( newElmId, ACTION_CONTENT.CREATE ); // Reload parent element in preview } else if ( self.isReloadParentElm( newElmId ) ) { self.reloadElmInPreview( self.getElmParentId( newElmId ) ); $usb.history.commitChange( newElmId, ACTION_CONTENT.CREATE ); // Add new element to preview } else { self.addElmToPreview( newElmId, index, parentId, _undefined, newElmId ); } // NOTE: A timeout is used to release the main thread. $ush.timeout( self.reloadElementsMap.bind( self ), 0 ); }, /** * Copy shortcode to clipboard. * * @event handler * @param {String} id Shortcode's usbid, e.g. "vc_row:1". */ _elmCopy: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return; } // Add copied text to buffer const content = $ush.toString( self.getElmShortcode( id ) ); $ush.copyTextToClipboard( content.replace( /\susbid="([^\"]+)"/gi, '' ) ); // NOTE: We will save the content in the storage unchanged, // and when adding it to the page, we will update all IDs. $ush.storage( 'usb' ).set( 'clipboard', content ); }, /** * Paste shortcode to content. * * @event handler * @param {String} id Shortcode's usbid, e.g. "vc_row:1". */ _elmPaste: function( id ) { const self = this; if ( ! self.isValidId( id ) && ! self.isRootContainer( id ) ) { return; } var content = $ush.toString( $ush.storage( 'usb' ).get( 'clipboard' ) ); if ( ! content ) { $usb.notify.add( $usb.getTextTranslation( 'empty_clipboard' ), NOTIFY_TYPE.INFO ); return; } // Remove rows or replace with "*_inner". if ( self.isRemoveRows ) { content = $usb.builder.removeRows( content ); } var newElmId; content = content // Remove all `el_id` from the design_options .replace( /(\s?el_id="([^\"]+)")/gi, '' ) // Replace all ids with current ones .replace( /usbid="([^\"]+)"/gi, ( _, elmId ) => { elmId = self.getSpareElmId( elmId ); if ( ! newElmId ) { newElmId = elmId; } return `usbid="${elmId}"`; } ); // Strict mode is a hard dependency between elements! // The check if the moved element is a TTA elements, section or vc_column(_inner), if so, then enable strict mode. const strictMode = ( self.isElmTTA( id ) || self.isChildElmContainer( id ) ); // Define the container into which the element will be added var parentId = id if ( ( self.isRow( newElmId ) && self.isRow( id ) ) || ( self.isRowInner( newElmId ) && self.isRowInner( id ) ) || ( self.isElmSection( newElmId ) && self.isElmSection( id ) ) ) { parentId = self.getElmParentId( id ); } // Check if the element can be a child of the hover element if ( ! self.canBeChildOf( newElmId, parentId, strictMode ) // NOTE: Only in this place is it allowed to add sections to the TTA && ! ( self.isElmSection( newElmId ) && self.isElmTTA( parentId ) ) ) { $usb.notify.add( $usb.getTextTranslation( 'cannot_paste' ), NOTIFY_TYPE.INFO ); return; } // Get index for new element var index = 0; if ( parentId !== id ) { index = $ush.parseInt( self.getElmIndex( id ) ) + 1; // next index // Section at the end } else if ( self.isElmTTA( parentId ) ) { index = self.getElmChildren( parentId ).length + 1; // end } // Add shortcodes to content if ( ! self.insertShortcodeIntoContent( parentId, index, content ) ) { $usb.notify.add( $usb.getTextTranslation( 'invalid_data' ), NOTIFY_TYPE.ERROR ); return; } self.reloadElementsMap(); // Reload element in preview if ( self.isReloadElm( parentId ) ) { self.reloadElmInPreview( parentId ); $usb.history.commitChange( newElmId, ACTION_CONTENT.CREATE ); // Reload parent element in preview } else if ( self.isReloadParentElm( newElmId ) ) { self.reloadElmInPreview( self.getElmParentId( newElmId ) ); $usb.history.commitChange( newElmId, ACTION_CONTENT.CREATE ); // Add new element to preview } else { self.addElmToPreview( newElmId, index, parentId ); } $ush.timeout( self.reloadElementsMap.bind( self ), 1 ); }, /** * Removes an element. * * @event handler * @param {String} id Shortcode's usbid, e.g. "us_btn:1" */ _elmDelete: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return; } // The check if this is the last column then delete the parent row* if ( self.isChildElmContainer( id ) && self.getElmSiblingsId( id ).length === 1 ) { id = self.getElmParentId( id ); } self.removeElm( id ); }, /** * Handler for data history changes; this method * is called every time the history data is updated. * * @event handler * @param {{}} task The task data object. */ historyChanged: function( task ) { const self = this; // Restore the element to the map if ( $.isPlainObject( task ) && task.type === HISTORY_TYPE.REDO ) { elementsMap.set( task.id, { parentId: task.parentId, elmIndex: task.index } ); } }, /** * Add a new row using text from the dropzone preview. */ dropzoneAddRow: function() { const self = this; self.createElm( 'vc_column_text', self.rootContainer, /* index */0, /* values */{}, self.elmSelected ); }, }); // Add new elements via Drag & Drop $.extend( Builder.prototype, { // The minimum movement threshold in pixels to initiate dragging. DRAG_SHIFT_THRESHOLD: 5, /** * Standard `dragstart` browser event handler * * @event handler * @param {Event} e * @return {Boolean} If the event occurs in context `MediaFrame`, then we will enable it, otherwise we will * disable it */ _dragstart: function( e ) { return $( e.target ).closest( '.media-frame' ).length > 0; }, /** * Init Drag & Drop. */ initDragDrop: function() { const self = this; if ( self.isDragDropInitialized() ) { return; } _tempData.dragDropInitialized = true; // Track events for Drag & Drop $usb.$document .on( 'pointerdown', self._events.maybeStartDrag ) .on( 'pointermove', self._events.maybeDrag ) .on( 'pointerup', self._events.endDrag ); // Reset all data by default for more reliable operation $usbcore.cache( 'drag' ).set( { startX: 0, startY: 0 } ); }, /** * Destroy Drag & Drop */ destroyDragDrop: function() { const self = this; if ( ! self.isDragDropInitialized() ) { return; } _tempData.dragDropInitialized = false; // Remove events $usb.$document .off( 'pointerdown', self._events.maybeStartDrag ) .off( 'pointermove', self._events.maybeDrag ) .off( 'pointerup', self._events.endDrag ); $usbcore.cache( 'drag' ).flush(); }, /** * Checks whether the Drag & Drop functionality is currently initialized. * * @returns {boolean} True if Drag & Drop is initialized, false otherwise. */ isDragDropInitialized: function() { return _tempData.dragDropInitialized; }, /** * Get the new element ID. * * @return {String} The unique id e.g. "us_btn:1" */ getNewElmId: function() { return $usbcore.cache( 'drag' ).get( 'newElmId', '' ); }, /** * Get the event data for send iframe * * @param {Event} e * @return {{}} The event data */ _getEventData: function( e ) { const self = this; if ( ! $usb.iframeIsReady ) { return; } // Get data on the coordinates of the mouse for iframe and relative to this iframe const rect = $ush.$rect( $usb.iframe ); const iframeWindow = $usb.iframe.contentWindow; const data = { clientX: e.clientX, clientY: e.clientY, eventX: e.pageX - rect.x, eventY: e.pageY - rect.y, pageX: ( e.pageX + iframeWindow.scrollX ) - rect.x, pageY: ( e.pageY + iframeWindow.scrollY ) - rect.y, }; // Additional check of values for errors for ( const prop in data ) { const value = data[ prop ] || NaN; if ( isNaN( value ) || value < 0 ) { data[ prop ] = 0; } else { data[ prop ] = ceil( data[ prop ] ); } } return data; }, /** * Dragging from left/right panel. * * @return {Boolean} */ draggingFromPanel: function() { return _tempData.draggingFromPanel === true; }, /** * Show the transit * * @param {String} elmName */ showTransit: function( elmName ) { const self = this; if ( ! elmName ) { return; } if ( self.hasTransit() ) { self.hideTransit(); } if ( self.isValidId( elmName ) ) { elmName = self.getElmType( elmName ); } // NOTE: `data-element-type` also lives on the element settings form // (`