/** * 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 // (`
`). Exclude it, otherwise elements with no Add-panel // tile (Inner Row / Inner Column) fall through to that form and the whole settings // panel gets cloned as the drag ghost. Small sources (panel tile, navigator row) remain. var target = _document.querySelector( `[data-element-type="${elmName}"]:not(.usb-panel-fieldset)` ); // Show custom transit for Templates or Favorite Section const isTemplate = $usb.templates.isTemplate( elmName ); const isFavoriteSection = $usb.favorites.isFavoriteSection( elmName ); if ( isTemplate ) { target = self.getCustomTransit( 'Template section' ); } else if ( isFavoriteSection ) { target = self.getCustomTransit( 'Favorite section' ); } if ( ! $ush.isNode( target ) ) { return; } // Object with intermediate data for transit const transit = { rect: $ush.$rect( target ), scrollAcceleration: 0, // scroll acceleration while drag scrollDirection: _undefined, // scroll directions while drag target: target.cloneNode( /* deep */true ) // copy of target to transit }; $usbcore.$removeClass( transit.target, 'hidden' ); if ( isTemplate || isFavoriteSection ) { self.hideCustomTransit(); } // Set the height and width of the transit element [ 'width', 'height' ].map( ( prop ) => { var value = ceil( transit.rect[ prop ] ); transit.target.style[ prop ] = value ? value + 'px' : 'auto'; } ); $usbcore .$addClass( transit.target, 'elm_transit' ) .$addClass( transit.target, ! self.isMode( 'drag_new_element' ) ? 'state_drag_move' : '' ); _document.body.append( transit.target ); _tempData.transit = transit; }, /** * Determines if transit * * @return {Boolean} True if transit, False otherwise */ hasTransit: function() { return !! _tempData.transit; }, /** * Get custom transit. * * @param {String} name Text is displayed in transit. * @return {Node} Returns the transit node. */ getCustomTransit: function( name ) { $usbcore.$removeClass( _tempData.customTransit, 'hidden' ); _tempData.customTransit.querySelector( '.for_name' ).innerText = $ush.toString( name ); return _tempData.customTransit; }, /** * Hide custom transit. */ hideCustomTransit: function() { $usbcore.$addClass( _tempData.customTransit, 'hidden' ); }, /** * Determines if drag scroll * * @return {Boolean} True if drag scroll, False otherwise */ hasDragScrolling: function() { return [ DIRECTION_TOP, DIRECTION_BOTTOM ].includes( ( _tempData.transit || {} ).scrollDirection ); }, /** * Set the transit position * * NOTE: The method is called many times, so performance is important here! * * @param {Number} pageX The event.pageX * @param {Number} pageY The event.pageY */ setTransitPosition: function( pageX, pageY ) { const self = this; if ( ! self.hasTransit() ) { return; } if ( ! $usb.navigator.isDragStarted() && ! self.isDragMode() ) { return; } const transit = _tempData.transit || {}; if ( ! $ush.isNode( transit.target ) ) { return; } // Get indents to transit center var isDragAdd = self.isMode( 'drag_new_element' ), transitHeight = transit.rect.height, transitTop = $ush.parseInt( isDragAdd ? pageY - ( transitHeight / 2 ) : pageY ), transitLeft = $ush.parseInt( isDragAdd ? pageX - ( transit.rect.width / 2 ) : pageX ); // Set transit center in under cursor transit.target.style.top = transitTop.toFixed( 3 ) + 'px'; transit.target.style.left = transitLeft.toFixed( 3 ) + 'px'; if ( ! $usb.iframeIsReady || $usb.navigator.isDragStarted() ) { return; } // Control auto-scroll preview when drag var remainderToEnd = 0, // Remainder to scroll end point (up|down) scrollDirection, // No value does not start animation viewportBottom = $ush.parseInt( _window.innerHeight - transitHeight ); // Get direction to scroll preview if ( pageY < transitHeight ) { remainderToEnd = abs( pageY - transitHeight ); scrollDirection = DIRECTION_TOP; } else if ( pageY > viewportBottom ) { remainderToEnd = abs( pageY - viewportBottom ); scrollDirection = DIRECTION_BOTTOM; } // NOTE: After pass every `step` pixels, the speed will increase by x1 ( speed / scrollAcceleration ) const scrollAcceleration = ceil( abs( remainderToEnd / /* acceleration step in px */30 ) ); // Transit data updates and scroll control if ( scrollDirection !== transit.scrollDirection || scrollAcceleration !== transit.scrollAcceleration ) { transit.scrollDirection = scrollDirection; transit.scrollAcceleration = scrollAcceleration; $usb.postMessage( 'doAction', [ /* method */'_scrollDragging', /* params */[ scrollDirection, scrollAcceleration ] ] ); } }, /** * Hide the transit */ hideTransit: function() { const self = this; const transit = _tempData.transit || {}; if ( ! self.hasTransit() || ! $ush.isNode( transit.target ) ) { return; } self.stopDragScrolling(); $usbcore.$remove( transit.target ); delete _tempData.transit; }, /** * Handles the start of element movement. * Determines whether an element should be moved. * * @event handler * @param {Event} e */ _maybeStartDrag: function( e ) { const targetEl = e.target; if ( ! $usb.iframeIsReady || ! targetEl || targetEl.classList.contains( 'usb_skip_draggable' ) ) { return; } // Use the native and highly optimized .closest() method for DOM traversal. // NOTE: Navigator rows also carry `data-element-type`, so we exclude them here. // The navigator has its own Drag & Drop (see navigator.js) that moves elements // via moveElm(); without this guard grabbing a navigator row would also start the // panel drag-to-create flow and spuriously insert a new element into the preview. const dragTarget = targetEl.closest( '[data-element-type]' ); if ( dragTarget && ! dragTarget.closest( '.usb-navigator' ) ) { $usbcore.cache( 'drag' ).set( { isDragStarted: true, startX: e.pageX || 0, startY: e.pageY || 0, target: dragTarget, } ); } }, /** * Handles possible drag start. * Called frequently, keep it optimized. * * @event handler * @param {Event} e */ _maybeDrag: function( e ) { const self = this; const dragData = $usbcore.cache( 'drag' ).data(); if ( ! dragData.isDragStarted || ! dragData.target ) { return; } // Get offsets from origin along axis X and Y const diffX = abs( dragData.startX - e.pageX ); const diffY = abs( dragData.startY - e.pageY ); // The check the distance of the mouse drag and if it is more than // the specified one, then activate all the necessary methods if ( diffX > self.DRAG_SHIFT_THRESHOLD || diffY > self.DRAG_SHIFT_THRESHOLD ) { if ( self.isMode( 'edit' ) ) { // Flush active move data $usbcore.cache( 'dragProcessData' ).flush(); // Set mode parent drag _tempData.draggingFromPanel = true; var dataElementType = $usbcore.$attr( dragData.target, 'data-element-type' ); self.setMode( 'drag_new_element' ); dragData.newElmId = self.getSpareElmId( dataElementType ); // Show the transit self.showTransit( dataElementType, e.pageX, e.pageY ); // Add helpers classes for visual control $usbcore .$addClass( dragData.target, 'draggable_element' ) .$addClass( _document.body, 'dragging_element' ); } // Firefox and Safari 17+ blocks events between current page and iframe so will use `onParentEventData` // Other browsers in iframe intercepts events if ( ( $ush.isFirefox || $ush.safariVersion() >= 17 ) && self.draggingFromPanel() ) { var eventData = self._getEventData( e ); if ( eventData.pageX ) { $usb.postMessage( 'onParentEventData', [ '_maybeDrop', eventData ] ); } } self.setTransitPosition( e.pageX, e.pageY ); } }, /** * Handles drag end. * * @event handler * @param {Event} e */ _endDrag: function( e ) { const self = this; if ( ! $usb.iframeIsReady ) { return; } const dragData = $usbcore.cache( 'drag' ).data(); if ( $ush.isNode( dragData.target ) ) { $usbcore .$removeClass( dragData.target, 'draggable_element' ) .$removeClass( _document.body, 'dragging_element' ); } // Case relevant only for FF when a new element has been dropped above // the panel and should not be added to the page if ( $usb.panel.isVisible() && $ush.isFirefox && $usb.preview.getCurrentOffset().x >= e.clientX ) { self._clearDragAssets(); return; } if ( ! self.draggingFromPanel() ) { $usbcore.cache( 'drag' ).flush(); return; } // Add new element if ( self.isMode( 'drag_new_element' ) && dragData.currentId && dragData.parentId ) { var insertIndex = $ush.parseInt( dragData.insertIndex ); // Get base parentId without alias if ( self.isAliasElmId( dragData.parentId ) ) { dragData.parentId = self.removeAliasFromId( dragData.parentId ); } // If the target has a template id, then continue processing as a template var templateId = $usbcore.$attr( dragData.target, 'data-template-id' ), favoriteSectionId = $usbcore.$attr( dragData.target, 'data-section-id' ); if ( templateId ) { var templateCategoryId = $( dragData.target ) .closest( '.usb-template' ) .data( 'template-category-id' ); $usb.templates.insertTemplate( templateCategoryId, templateId, dragData.parentId, insertIndex ); } else if ( favoriteSectionId ) { $usb.favorites.insertSection( favoriteSectionId, dragData.parentId, insertIndex ); } else { // Create and add a new element self.createElm( self.getElmType( dragData.currentId ), dragData.parentId, insertIndex ); } // If the final container is a TTA section then open this section if ( self.isElmSection( dragData.parentId ) ) { $usb.postMessage( 'doAction', [ 'openSectionById', dragData.parentId ] ); } } // Firefox and Safari 17+ blocks events between current page and frame so will use onParentEventData // Other browsers in iframe intercepts events if ( $ush.isFirefox || $ush.safariVersion() >= 17 ) { $usb.postMessage( 'onParentEventData', '_endDrag' ); } self._clearDragAssets(); }, /** * Clear all asset and cache data to `drag_new_element` */ _clearDragAssets: function() { const self = this; self.hideTransit(); _tempData.draggingFromPanel = false; $usbcore.cache( 'drag' ).flush(); self.setMode( 'edit' ); $usb.postMessage( 'doAction', 'clearDragData' ); }, /** * Remove a drag scroll data */ removeDragScrollData: function() { delete ( _tempData.transit || {} ).scrollDirection; }, /** * Stop a drag scroll */ stopDragScrolling: function() { const self = this; if ( ! self.hasDragScrolling // Fix weird missing method error || ! self.hasDragScrolling() ) { return; } self.removeDragScrollData(); $usb.postMessage( 'doAction', '_scrollDragging' ); }, /** * Temporarily removes an element from the active map and stashes it. * @param {string|number} elementId */ excludeElement: function( elementId ) { if ( elementsMap.has( elementId ) ) { const elementData = elementsMap.get( elementId ); if ( $ush.isUndefined( elementData ) ) { return; } _tempData.excludedElements[ elementId ] = elementData; elementsMap.delete( elementId ); } }, /** * Checks if there are any temporarily excluded elements. * @returns {boolean} */ hasExcludedElements: function() { return Object.keys( _tempData.excludedElements ).length > 0; }, /** * Retrieves a specific excluded element by its ID. * @param {string|number} elementId * @returns {{}|null} Element data or null if not found. */ getExcludedElement: function( elementId ) { return _tempData.excludedElements[ elementId ] || null; }, /** * Restores all temporarily excluded elements back to the active map. */ includeExcludedElements: function() { if ( this.hasExcludedElements() ) { Object .entries( _tempData.excludedElements ) .forEach( ( [ elementId, data ] ) => elementsMap.set( elementId, data ) ); _tempData.excludedElements = {}; } }, } ); // Builder API $.extend( Builder.prototype, { /** * Reload elements Map. * High performance implementation with optimized memory allocation. * * @return {Promise} */ reloadElementsMap: async function() { const self = this; // A full map rebuild is authoritative, so any drag-time exclusion stash is now stale. // Drop it — otherwise entries left over from previous drags get re-excluded by // canBeChildOf() and silently delete currently-valid elements from the map, which // breaks drop-target validation (e.g. the navigator's dropplace indicator vanishes). _tempData.excludedElements = {}; // A rebuild is authoritative, so wipe the map BEFORE the empty-content early return — // otherwise deleting all page content leaves the map full of stale (deleted) element IDs // that navigator redraws and drop-target validation then treat as still present. elementsMap.clear(); const content = $ush.toString( self.pageData.content ); if ( ! content ) { return; } const allShortcodesRegexp = self.getShortcodeRegex('[\\dA-Za-z_-]+'); const parseShortcode = ( innerContent, parentId, depth = 0 ) => { if ( depth > 1000 ) { return; } var elementIndex = 0; const matchesIterator = innerContent.matchAll( allShortcodesRegexp ); for ( const match of matchesIterator ) { const fullMatchString = match[3]; if ( ! fullMatchString ) { continue; } // Fast extraction of 'usbid' value using native string methods const targetAttr = 'usbid="'; const attributeIndex = fullMatchString.indexOf( targetAttr ); if ( attributeIndex === -1 ) { continue; } const valueStartIndex = attributeIndex + targetAttr.length; const valueEndIndex = fullMatchString.indexOf( '"', valueStartIndex ); if ( valueEndIndex === -1 ) { continue; } const elementId = fullMatchString.slice( valueStartIndex, valueEndIndex ); if ( ! elementId ) { continue; } const innerShortcodeContent = match[5]; if ( innerShortcodeContent ) { const sanitizedContent = $ush.toString( innerShortcodeContent ); parseShortcode( sanitizedContent, elementId, depth + 1 ); } elementsMap.set( elementId, { parentId: parentId, elmIndex: elementIndex++ } ); } }; parseShortcode( content, self.rootContainer ); }, /** * Check if the given id is an element identifier. * * @param {String} id Shortcode id (e.g. 'vc_row:1') * @return {Boolean} True if it is an element identifier, otherwise false */ hasElementId: function( id ) { return ! ! ( id && elementsMap.has( id ) ); }, /** * Check if saving is in progress. * * @return {Boolean} True if saving is in progress, otherwise false */ isSaving: function() { return _tempData.isSaving; }, /** * Save page data to the server. * * @param {Function} complete Callback on completion */ savePageData: function( complete ) { const self = this; // The page data var data = { // The available key=>value: // post_content: '', // post_status: '' , // post_title: '', // postMeta: [ key => value ] postMeta: {}, }; // Add updated content if ( self.isContentChanged() ) { data.post_content = self.pageData.content; } if ( self.isPageFieldsChanged() ) { for ( const prop in self.pageData.fields ) { data[ prop ] = self.pageData.fields[ prop ]; } } // Add updated meta data if ( self.isPostMetaChanged() ) { for ( const prop in self.pageData.postMeta ) { data.postMeta[ prop ] = self.pageData.postMeta[ prop ]; } } if ( self.isPageCustomCssChanged() ) { data.postMeta[ $usb.config( 'keyCustomCss', '' ) ] = self.pageData.customCss; } // Post content with remove rows if ( self.isRemoveRows && self.isPostContentRootContainer( self.rootContainer ) ) { data.remove_rows = 1; } _tempData.isSaving = true; // Send data to server $usb.ajax( '_savePageData', { data: $.extend( data, { _nonce: $usb.config( '_nonce' ), action: $usb.config( 'action_save_post' ), } ), success: ( res ) => { if ( ! res.success ) { return; } $usb.notify.add( $usb.getTextTranslation( 'page_updated' ), NOTIFY_TYPE.SUCCESS ); // Reload preview page if ( self._isReloadPreviewAfterSave && ( self.isPageFieldsChanged() || self.isPostMetaChanged() ) ) { self._isReloadPreviewAfterSave = false; $usb.reloadPreview(); } _tempData.savedPageData = $ush.clone( self.pageData ); }, complete: () => { if ( typeof complete === 'function' ) { complete(); } _tempData.isSaving = false; } } ); }, /** * Check if content has changed. * * @return {Boolean} True if changed, otherwise false */ isContentChanged: function() { return ( _tempData.savedPageData.content || '' ) !== ( this.pageData.content || '' ); }, /** * Check if page custom CSS has changed. * * @return {Boolean} True if changed, otherwise false */ isPageCustomCssChanged: function() { return ( _tempData.savedPageData.customCss || '' ) !== ( this.pageData.customCss || '' ); }, /** * Check if page fields have changed. * * @return {Boolean} True if changed, otherwise false */ isPageFieldsChanged: function() { return ! $ush.comparePlainObject( _tempData.savedPageData.fields, this.pageData.fields ); }, /** * Check if post metadata has changed. * * @return {Boolean} True if changed, otherwise false */ isPostMetaChanged: function() { return ! $ush.comparePlainObject( _tempData.savedPageData.postMeta, this.pageData.postMeta ); }, /** * Check if the post has changed. * * @return {Boolean} True if changed, otherwise false */ isPageChanged: function() { const self = this; return ( self.isContentChanged() || self.isPostMetaChanged() || self.isPageFieldsChanged() || self.isPageCustomCssChanged() ); }, /** * Check if the page content is empty. * * @return {Boolean} True if empty, otherwise false */ isEmptyContent: function() { return ! /\[(vc|us)_/.test( String( this.pageData.content ) ); }, /* * Check if the value is a responsive object. * * @param {*} value The value * @return {Boolean} True if it is a responsive object, otherwise false */ isResponsiveObject: function( value ) { if ( ! $.isPlainObject( value ) ) { return false; } // Get responsive states const states = $usb.config( 'responsiveStates', [] ); for ( const i in states ) if ( value.hasOwnProperty( states[ i ] ) ) { return true; } return false; }, /** * Determines if mode. * * As parameters, you can set both one mode and several to check for matches, * if at least one of the results matches, then it will be true * * @return {Boolean} Returns true if there is a mode, otherwise false */ isMode: function( mode ) { // Fast path: a single string mode is the common case (incl. pointermove hot paths) — // compare directly and avoid allocating a throwaway array on every call. if ( typeof mode === 'string' ) { return mode === _activeMode; } if ( ! Array.isArray( mode ) ) { mode = [ $ush.toString( mode ) ]; } return mode.includes( _activeMode ); }, /** * Determines if a drag & drop operation is currently in progress. * * @return {Boolean} True if the active mode is one of DRAG_MODES. */ isDragMode: function() { return DRAG_MODES.includes( _activeMode ); }, /** * Set the mode. * * @param {String} mode The mode. * @return {Boolean} True if mode changed successfully, False otherwise. */ setMode: function( mode ) { const self = this; if ( mode && BUILDER_MODES.includes( mode ) && mode !== _activeMode ) { $usb.trigger( 'builder.modeChanged', _activeMode = mode ); return true; } return false; }, /** * Generate a RegExp to identify a shortcode * * NOTE: RegExp does not know how to work with neste the shortcode in itself. * * Capture groups: * * 1. An extra `[` to allow for escape shortcodes with double `[[]]` * 2. The shortcode name * 3. The shortcode argument list * 4. The self close `/` * 5. The content of a shortcode when it wraps some content * 6. The close tag * 7. An extra `]` to allow for escape shortcodes with double `[[]]` * * @param {String} tag The shortcode tag "us_btn" or "vc_row|vc_column|..." * @return {RegExp} The elm shortcode regular expression */ getShortcodeRegex: function( tag ) { return new RegExp( '\\[(\\[?)(' + tag + ')(?![\\w-])([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*(?:\\[(?!\\/\\2\\])[^\\[]*)*)(\\[\\/\\2\\]))?)(\\]?)', 'g' ); }, /** * Remove html from start and end content * * @param {String} content * @return {String} */ removeHtmlWrap: function( content ) { return $ush.toString( content ).replace( /^<[^\[]+|[^\]]+$/gi, '' ); }, /** * Parse shortcode text in parts * * @param {String} content * @return {{}} */ parseShortcode: function( content ) { const self = this; content = self.removeHtmlWrap( content ); if ( ! content ) { return {}; } const firstTag = ( content.match( /^.*?\[([\w\-]+)\s/ ) || [] )[1] || ''; const matches = ( self.getShortcodeRegex( firstTag ) ).exec( content ); if ( matches ) { return { tag: matches[2], atts: self._unescapeAttr( matches[3] || '' ), input: matches[0], content: matches[5] || '', hasClosingTag: !! matches[6] }; } return {}; }, /** * Convert attributes from string to object * * @param {String} str * @return {{}} */ parseAtts: function( str ) { const result = {}; if ( ! str ) { return result; } // Map zero-width spaces to actual spaces str = str.replace( /[\u00a0\u200b]/g, ' ' ); // The retrieve attributes from a string ( str.match( /[\w-_]+="([^\"]+)?"/g ) || [] ).forEach( ( attribute ) => { attribute = attribute.match( /([\w-_]+)="([^\"]+)?"/ ); if ( ! attribute ) { return; } // Restoring escaped values from a shortcode attribute const value = $ush.toString( attribute[2] ) .replace( /``/g, '"' ) .replace( /`{`/g, '[' ) .replace( /`}`/g, ']' ); result[ attribute[1] ] = value.trim(); }); return result; }, /** * Converts a shortcode object to a string * * @param {{}} object The shortcode object * @param {{}} attsDefaults The default atts * @return {String} */ buildShortcode: function( shortcode, attsDefaults ) { const self = this; if ( $.isEmptyObject( shortcode ) ) { return ''; } var result = '[' + shortcode.tag; // The add attributes if ( shortcode.atts || attsDefaults ) { if ( ! $.isEmptyObject( attsDefaults ) ) { shortcode.atts = self.buildAtts( self.parseAtts( shortcode.atts ), attsDefaults ); } // Escape for shortcode attributes shortcode.atts = self._escapeAttr( shortcode.atts ); result += ' ' + shortcode.atts.trim(); } result += ']'; // The add content if ( shortcode.content ) { result += shortcode.content; } // The add end tag if ( shortcode.hasClosingTag ) { result += '[/'+ shortcode.tag +']'; } return '' + result; }, /** * Returns a string representation of an attributes * * @param {{}} atts This is an attributes object * @param {{}} defaults The default atts * @return {String} String representation of the attributes */ buildAtts: function( atts, defaults ) { if ( ! atts || $.isEmptyObject( atts ) ) { return ''; } if ( $.isEmptyObject( defaults ) ) { defaults = {}; } const result = []; for ( const k in atts ) { var value = atts[ k ]; // Check the values for correctness, otherwise we will skip the additions if ( value === null || $ush.isUndefined( value ) || ( ! $ush.isUndefined( defaults[ k ] ) && defaults[ k ] === value ) ) { continue; } // Convert param list to string (for wp link) if ( $.isPlainObject( value ) ) { var inlineValue = []; for ( var i in value ) { if ( value[ i ] ) { inlineValue.push( i + ':' + value[ i ] ); } } value = inlineValue.join('|'); } // Escaping reserved values for a shortcode attribute value = $ush.toString( value ) .replace( /\"/g, '``' ) .replace( /\[/g, '`{`' ) .replace( /\]/g, '`}`' ); result.push( k + '="' + value + '"' ); } return result.join( ' ' ); }, /** * Check if the given id is valid. * * @param {String} id Shortcode id (e.g. 'us_btn:1') * @return {Boolean} True if the id is valid, otherwise false */ isValidId: function( id ) { return id && /^([\w\-]+):(\d+)(\|[a-z\-]+)?$/.test( id ); }, /** * Check if the given id is a row. * * @param {String} id Shortcode id (e.g. 'vc_row:1') * @return {Boolean} True if it is a row, otherwise false */ isRow: function( id ) { return this.getElmName( id ) === 'vc_row'; }, /** * Check if the given id is an inner row. * * @param {String} id Shortcode id (e.g. 'vc_row_inner:1') * @return {Boolean} True if it is an inner row, otherwise false */ isRowInner: function( id ) { return this.getElmName( id ) === 'vc_row_inner'; }, /** * Check if the given id is a column (including inner columns). * * @param {String} id Shortcode id (e.g. 'vc_column:1') * @return {Boolean} True if it is a column, otherwise false */ isColumn: function( id ) { return /^vc_column(_inner)?$/.test( this.getElmName( id ) ); }, /** * Check whether the specified ID belongs to a content carousel. * * @param {*} id Element identifier. * @return {boolean} True if the ID belongs to a content carousel. */ isContentCarousel: function( id ) { return this.getElmName( id ) === 'content_carousel'; }, /** * Check whether the specified ID is a single-slide content carousel. * * @param {*} id Element identifier. * @return {boolean} True if the ID is a single-slide content carousel. */ isContentCarouselSingleSlide: function( id ) { return this.isContentCarousel( id ) && this.getElmShortcode( id ).includes( 'items="1"' ); }, /** * Check if the given id is outside the root container. * * @param {String} id Shortcode id (e.g. 'us_header:1') * @return {Boolean} True if it is outside the root container, otherwise false */ isOutsideRootContainer: function( id ) { return $usb.config( 'elms_outside_root_container', [] ).includes( this.getElmName( id ) ); }, /** * Check if the given id is the default root container. * * @param {String} id Shortcode id (e.g. 'root_container') * @return {Boolean} True if it is the default root container, otherwise false */ isDefaultRootContainer: function( id ) { return id && id === this.defaultRootContainer; }, /** * Check if the given id is a post content root container. * * @param {String} id Shortcode id (e.g. 'post_content_root_container') * @return {Boolean} True if it is a post content container, otherwise false */ isPostContentRootContainer: function( id ) { return id && id === this.postContentRootContainer; }, /** * Check if the given id is the root container * (stored in `self.rootContainer`, e.g. 'root_container' or 'post_content_root_container'). * * @param {String} id Shortcode id (e.g. 'root_container') * @return {Boolean} True if it is the root container, otherwise false */ isRootContainer: function( id ) { return this.isDefaultRootContainer( id ) || this.isPostContentRootContainer( id ); }, /** * Set the root container. * * @param {String} rootContainer Root container */ setRootContainer: function( rootContainer ) { const self = this; if ( ! [ self.defaultRootContainer, self.postContentRootContainer ].includes( rootContainer ) ) { rootContainer = self.defaultRootContainer; } if ( self.rootContainer !== rootContainer ) { self.rootContainer = rootContainer; self.reloadElementsMap(); } }, /** * Check if the given id is a container. * * @param {String} id Shortcode id (e.g. 'vwrapper:1') * @return {Boolean} True if it is a container, otherwise false */ isElmContainer: function( name ) { const self = this; if ( self.isValidId( name ) ) { name = self.getElmName( name ); } return $usb.config( 'shortcode.containers', [] ).includes( name ); }, /** * Check if the given id is a node root container * (e.g. `vc_row`, `vc_row_inner`, `vc_tta_tabs`, `vc_tta_accordion`, etc.). * * @param {String} id Shortcode id (e.g. 'us_btn:1') * @return {Boolean} True if it is a root container, otherwise false */ isRootElmContainer: function( name ) { const self = this; if ( self.isValidId( name ) ) { name = self.getElmName( name ); } return ( self.isElmContainer( name ) && !! $usb.config( `shortcode.relations.as_parent.${name}.only` ) ); }, /** * Check if the given id is a second-level container * (e.g. `vc_column`, `vc_column_inner`, `vc_tta_section`, etc.). * * @param {String} id Shortcode id (e.g. 'us_btn:1') * @return {Boolean} True if it is a second-level container, otherwise false */ isChildElmContainer: function( name ) { const self = this; if ( self.isValidId( name ) ) { name = self.getElmName( name ); } return ( self.isElmContainer( name ) && ! self.isRootElmContainer( name ) && !! $usb.config( `shortcode.relations.as_child.${name}.only` ) ); }, /** * Check whether the element should be reloaded when its content changes. * * @param {String|Node} elmId Shortcode id (e.g. 'us_content_carousel:1') * @return {Boolean} True if the element requires parent update, otherwise false */ isReloadElm: function( elmId ) { const self = this; if ( $ush.isNode( elmId ) ) { elmId = self.getElmId( elmId ); } if ( ! self.isValidId( elmId ) ) { return false; } return $usb.config( 'shortcode.reload_element', [] ).includes( self.getElmName( elmId ) ); }, /** * Check whether the parent element needs to be reloaded. * * @param {String|Node} elmId Shortcode id (e.g. 'vc_tta_section:1') * @return {Boolean} True if the parent requires update, otherwise false */ isReloadParentElm: function( elmId ) { const self = this; if ( $ush.isNode( elmId ) ) { elmId = self.getElmId( elmId ); } if ( ! self.isValidId( elmId ) ) { return false; } return $usb.config( 'shortcode.reload_parent_element', [] ).includes( self.getElmName( elmId ) ); }, /** * Check if the given name is a TTA element. * * TTA = Tabs, Tour, Accordion. * * @param {String} name Element name (e.g. 'vc_tta_tabs:1') * @return {Boolean} True if it is a TTA element, otherwise false */ isElmTTA: function( name ) { const self = this; if ( self.isValidId( name ) ) { name = self.getElmType( name ); } return /^vc_tta_(tabs|tour|accordion)$/.test( name ); }, /** * Check if the given name is tabs or tour. * * @param {String} name Element name (e.g. 'vc_tta_tabs:1') * @return {Boolean} True if it is tabs or tour, otherwise false */ isElmTab: function( name ) { const self = this; if ( self.isValidId( name ) ) { name = self.getElmType( name ); } return /^vc_tta_(tabs|tour)$/.test( name ); }, /** * Check if the given name is a TTA section. * * @param {String} name Element name * @return {Boolean} True if it is a TTA section, otherwise false */ isElmSection: function( name ) { const self = this; if ( self.isValidId( name ) ) { name = self.getElmType( name ); } return name === 'vc_tta_section'; }, /** * Check if the specified name is an element wrapper. * * @param {Object} name Element name. * @return {boolean} True if the name is an element wrapper. */ isElmWrapper: function( name ) { const self = this; if ( self.isValidId( name ) ) { name = self.getElmType( name ); } return /^us_(h|v)wrapper$/.test( name ); }, /** * Escape for shortcode attributes. * * @param {String} value The value. * @return {String} Returns a string from escaped with special characters. */ _escapeAttr: function( value ) { return $ush.toString( value ) .replace( /\[/g, '[' ) .replace( /\]/g, ']' ); }, /** * Unescape for shortcode attributes. * * @param {String} value The value. * @return {String} Returns a string from the canceled escaped special characters. */ _unescapeAttr: function( value ) { return $ush.toString( value ) .replace( /[/g, '[' ) .replace( /]/g, ']' ); }, /** * Check the possibility of move the shortcode to the specified parent * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @param {String} parent Shortcode's usbid, e.g. "vc_column:1" * @param {Boolean} strict The ON/OFF strict mode * @return {Boolean} True if able to be child of, False otherwise */ canBeChildOf: function( id, parent, strict ) { const self = this; const isRootContainer = self.isRootContainer( parent ); if ( self.isRootContainer( id ) || ! self.isValidId( id ) || ! ( self.isValidId( parent ) || isRootContainer ) ) { return false; } if ( self.isMode( 'drag_from_preview' ) && self.isPostContentRootContainer( parent ) && ! self.isRow( id ) ) { return false; } const shortcodeRelations = $usb.config( 'shortcode.relations', {} ); if ( $.isEmptyObject( shortcodeRelations ) ) { return true; } const name = self.getElmName( id ); const parentName = isRootContainer ? parent : self.getElmName( parent ); const isDragNewElementMode = self.isMode( 'drag_new_element' ); const isRootContainerParent = self.isRootContainer( parentName ); const isChildElmContainer = self.isChildElmContainer( id ); return self._cacheDragProcessData( () => { const checkTypes = ['as_child', 'as_parent']; for ( var i = 0; i < checkTypes.length; i++ ) { const type = checkTypes[ i ]; const currentName = ( type === 'as_child' ) ? name : parentName; const relations = shortcodeRelations[ type ]?.[ currentName ]; if ( ! relations ) { continue; } if ( isDragNewElementMode && isRootContainerParent && ! isChildElmContainer ) { continue; } const targetName = ( type === 'as_child' ) ? parentName : name; if ( relations.only !== undefined ) { const allowed = relations.only.split( ',' ); if ( ! allowed.includes( targetName ) ) { return false; } } if ( relations.except !== undefined ) { const blocked = relations.except.split( ',' ); if ( blocked.includes( targetName ) ) { return false; } } } if ( strict && ( isRootContainer || isChildElmContainer ) ) { const hasExcludedElements = self.hasExcludedElements() && ( self.isMode( 'drag_from_preview' ) || $usb.navigator.isDragStarted() ); if ( hasExcludedElements ) { const excludedElements = { ..._tempData.excludedElements }; self.includeExcludedElements(); const elmParentId = self.getElmParentId( id ); _tempData.excludedElements = { ...excludedElements }; for ( const elmId in excludedElements ) { self.excludeElement( elmId ); } return parent === elmParentId; } return parent === self.getElmParentId( id ); } return true; }, `canBeChildOf:${id}|${parent}|${strict}`, false ); }, /** * Determine has same type parent * * NOTE: The method is called many times, so performance is important here! * * @param {String} type The tag type "us_btn|us_btn:1" * @param {String} parent Shortcode's usbid, e.g. "vc_column:1" * @return {Boolean} True if able to be parent of, False otherwise */ hasSameTypeParent: function( type, parent ) { const self = this; if ( self.isRootContainer( type ) || self.isRootContainer( parent ) || ! self.isValidId( parent ) ) { return false; } if ( self.isValidId( type ) ) { type = self.getElmType( type ); } if ( type === self.getElmType( parent ) ) { return true; } // Search all parents var iteration = 0; while( parent !== null || self.isRootContainer( parent ) ) { if ( iteration++ >= 1000 ) { break; } parent = self.getElmParentId( parent ); if ( self.getElmType( parent ) === type ) { return true; } } return false; }, /** * Get a valid container ID * * @param {*} container The container * @return {String} Returns a valid container in any case (on error it's rootContainer) */ getValidContainerId: function( container ) { return this.isElmContainer( container ) ? container : this.rootContainer; }, /** * Determines whether the specified ID is alias usbid * * @param {String} id Shortcode's usbid, e.g. "vc_tta_section:0|alias" * @return {Boolean} True if the specified id is alias usbid, False otherwise */ isAliasElmId: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return false; } return REGEXP_USBID_ALIAS.test( id ); }, /** * Get alias from ID * * NOTE: For any usbid, several aliases can be created that will still refer to the main usbid. * This allows you to implement functionality for specific elements, for example: transfer * features from sections to tab buttons * * @param {String} id Shortcode's usbid, e.g. "vc_tta_section:0|alias" * @return {String|null} Returns the alias name if any, otherwise null */ getAliasFromId: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return null; } return ( id.match( REGEXP_USBID_ALIAS ) || [] )[2] || null; }, /** * Add alias to ID * * @param {String} alias The alias e.g. "alias-name" * @param {String} id Shortcode's usbid, e.g. "vc_tta_section:0" * @return {String} Returns the id from the appended alias */ addAliasToElmId: function( alias, id ) { const self = this; const args = arguments; if ( alias && typeof alias === 'string' && self.isValidId( id ) ) { id += '|' + alias; } else { $usb.log( 'Notice: Failed to add alias to id', args ); } return id; }, /** * Remove an alias from ID * * @param {String} id Shortcode's usbid, e.g. "vc_tta_section:0|alias" * @return {String} Returns id without alias */ removeAliasFromId: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return id; } return ( id.match( REGEXP_USBID_ALIAS ) || [] )[1] || id; }, /** * Get the elm type * * @param {String|Node} id Shortcode's usbid, e.g. "us_btn:1" * @return {String} The elm type */ getElmType: function( id ) { const self = this; if ( $ush.isNode( id ) ) { id = self.getElmId( id ); } return self.isValidId( id ) ? id.split(':')[0] || '' : ''; }, /** * Get the elm name * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @return {String} Returns the name of the element (without index) */ getElmName: function( id ) { const type = this.getElmType( id ); return ( type.match( /us_(.*)/ ) || [] )[1] || type; }, /** * Get the elm title * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @return {String} */ getElmTitle: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return 'Unknown'; } const name = self.getElmName( id ); return $usb.config( `elm_titles.${name}` ) || name; }, /** * Check if a shortcode with a given name exists or not * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @return {Boolean} Returns True if id exists, otherwise returns False */ doesElmExist: function( id ) { const self = this; if ( ! self.isValidId( id ) || elementsMap.size === 0 ) { return false; } return elementsMap.has( id ); }, /** * Get the elm id * * NOTE: The method is called many times, so performance is important here! * * @param {Node} node The target element * @return {String} id Shortcode's usbid, e.g. "us_btn:1" */ getElmId: function( node ) { const self = this; if ( ! $ush.isNode( node ) ) { return ''; } if ( ! node.hasOwnProperty( '_$$usbid' ) ) { const id = $usbcore.$attr( node, 'data-usbid' ); node._$$usbid = ( self.isValidId( id ) || self.isRootContainer( id ) ) ? id : ''; } return node._$$usbid; }, /** * Get the index of an element by ID * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @return {Number|null} The index of the element (Returns `null` in case of an error) */ getElmIndex: function( id ) { const self = this; if ( ! self.isValidId( id ) ) { return null; } const index = ( self.getElmSiblingsId( id ) || [] ).indexOf( id ); return index > -1 ? index : null; }, /** * Generate a spare shortcode usbid for a new element * * @param {String} type The type or usbid from which the type will be derived * @return {String} */ getSpareElmId: function( type ) { const self = this; if ( ! type ) { return ''; } if ( self.isValidId( type ) ) { type = self.getElmType( type ); } if ( ! Array.isArray( _tempData.usedElementIds ) ) { _tempData.usedElementIds = []; } for ( var index = 1;; index++ ) { const newElementId = type + ':' + index; if ( ! self.doesElmExist( newElementId ) && ! _tempData.usedElementIds.includes( newElementId ) ) { _tempData.usedElementIds.push( newElementId ); return newElementId; } } }, /** * Get element's direct parent's ID or a 'rootContainer' if element is at the root * * NOTE: The method is called many times, so performance is important here! * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @return {String|Boolean|null} Returns the parent id if successful, otherwise null or False */ getElmParentId: function ( id ) { const self = this; if ( ! self.doesElmExist( id ) || id === self.rootContainer ) { return null; } if ( elementsMap.has( id ) ) { return elementsMap.get( id ).parentId || null; } return null; }, /** * Get the element siblings id * * @param {String} id The id e.g. "us_btn:1" * @return {[]} The element siblings id */ getElmSiblingsId: function( id ) { const self = this; if ( ! self.isValidId( id ) || self.isRootContainer( id ) ) { return []; } return self.getElmChildren( self.getElmParentId( id ) ); }, /** * Get element's direct children IDs (or empty array, if element doesn't have children) * * NOTE: The method is called many times, so performance is important here! * * @param {String} id Shortcode's usbid, e.g. "vc_row:1" * @return {[]} Returns an array of child IDs */ getElmChildren: function( id ) { const self = this; const result = []; if ( id ) { elementsMap.forEach( ( elmData, elmId ) => { if ( elmData.parentId === id ) { result[ elmData.elmIndex ] = elmId; } } ); } return result.filter( ( val ) => { return val; } ); }, /** * Get all element's direct children IDs (or empty array, if element doesn't have children) * * @param {String} id Shortcode's usbid, e.g. "vc_row:1" * @return {[]} */ getElmAllChildren: function( id ) { const self = this; if ( ! self.isValidId( id ) || ! self.isElmContainer( id ) ) { return []; } const args = $ush.toArray( arguments ); const childrenIDs = self.getElmChildren( id ); var depth = $ush.parseInt( args[1] ), result = []; for ( const i in childrenIDs ) { const childrenId = childrenIDs[i]; if ( ! self.isValidId( childrenId ) ) { continue; } result.push( childrenId ); if ( self.isElmContainer( childrenId ) ) { if ( depth >= 20 ) { $usb.log( 'Notice: Exceeded number of levels in recursion:', args ); } else { result = result.concat( self.getElmAllChildren( childrenId, depth++ ) ); } } } return result; }, /** * Get element's shortcode (with all the children if they exist) * * @param {String} id Shortcode's usbid (e.g. "us_btn:1") * @return {String} */ getElmShortcode: function( id ) { const self = this; const content = $ush.toString( self.pageData.content ); if ( $ush.isUndefined( id ) ) { return content; } if ( ! self.isValidId( id ) ) { return ''; } const matches = content.match( self.getShortcodeRegex( self.getElmType( id ) ) ); if ( matches ) { for ( const i in matches ) { if ( matches[ i ].indexOf( `usbid="${id}"` ) !== -1 ) { return matches[ i ]; } } } return ''; }, /** * Get an node or nodes by ID * * @param {String|[]} id Shortcode's usbid, e.g. "us_btn:1" * @return {null|Node|[Node..]} */ getElmNode: function( id ) { const self = this; if ( ! $usb.iframeIsReady ) { return null; } return ( $usb.iframe.contentWindow.$usbp || {} ).getElmNode( id ); }, /** * Get all html for a node include styles * * @param {String|[]} id Shortcode's usbid, e.g. "us_btn:1" * @return {String} */ getElmOuterHtml: function( id ) { const self = this; if ( ! $usb.iframeIsReady ) { return ''; } return ( $usb.iframe.contentWindow.$usbp || {} ).getElmOuterHtml( id ) || ''; }, /** * Get shortcode's params values * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @return {{}} */ getElmValues: function( id ) { const self = this; if ( ! self.doesElmExist( id ) ) { return {}; } const shortcode = self.parseShortcode( self.getElmShortcode( id ) ); if ( ! $.isEmptyObject( shortcode ) ) { var result = self.parseAtts( shortcode.atts ), elmName = self.getElmName( id ); // Add content value to the result var editContent = $usb.config( 'shortcode.edit_content', {} ); if ( !! editContent[ elmName ] ) { result[ editContent[ elmName ] ] = '' + shortcode.content; } return result; } return {}; }, /** * Get shortcode param value by key name * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @param {String} key This is the name of the parameter * @param {*} defaultValue The default value * @return {*} */ getElmValue: function( id, key, defaultValue ) { return this.getElmValues( id )[ key ] || defaultValue; }, /** * Set shortcode's params values * * @param {String} id Shortcode's usbid, e.g. "us_btn:1" * @param {{}} values */ setElmValues: function( id, values ) { const self = this; if ( ! self.doesElmExist( id ) || $.isEmptyObject( values ) ) { return; } const shortcode = self.getElmShortcode( id ); const shortcodeObj = self.parseShortcode( shortcode ); if ( $.isEmptyObject( shortcodeObj ) ) { return; } shortcodeObj.atts = ' ' + self.buildAtts( $.extend( self.getElmValues( id ), values ) ); self.pageData.content = $ush.toString( self.pageData.content ).replace( shortcode, self.buildShortcode( shortcodeObj ) ); $usb.trigger( 'builder.contentChange' ); }, /** * Remove rows/columns or convert them to inner elements (*_inner). * * @param {String} content Shortcode content. * @return {String} Processed content with removed or converted elements. */ removeRows: function( content ) { content = String( content ); if ( content.includes( 'vc_row_inner' ) ) { return content.replace( /\[\/?vc_(?:row|column)\b[^\]]*\]/g, '' ); } return content .replace( /(\[\/?vc_(?:row|column)\b)([^\]]*\])/g, '$1_inner$2' ) .replace( /"(vc_(?:row|column)\b)(:[\d]+)"/g, '"$1_inner$2"' ); }, /** * Cached data as part of the drag & drop process * * NOTE: The method caches data only during the move, after which everything is deleted * * @param {Function} callback The callback function to get the result * @param {String} key The unique key to save data * @param {*} defaultValue The default value if no result * @return {*} Returns the result from the cache or the result of a callback function */ _cacheDragProcessData: function( callback, key, defaultValue ) { const self = this; if ( typeof callback !== 'function' ) { return defaultValue; } if ( self.isDragMode() ) { return $usbcore .cache( 'dragProcessData' ) .get( key, callback ); } return callback.call( self ); }, /** * Rendered shortcode * * @param {String} requestId The request id * @param {{}} settings A set of key/value pairs that configure the Ajax request */ renderShortcode: function( requestId, settings ) { const self = this; if ( ! requestId || $.isEmptyObject( settings ) ) { return; } if ( ! $.isPlainObject( settings.data ) ) { settings.data = {}; } // Add required settings $.extend( settings.data, { _nonce: $usb.config( '_nonce' ), action: $usb.config( 'action_render_shortcode' ) } ); // Content preparation if ( $ush.isUndefined( settings.data.content ) ) { settings.data.content = ''; } else { settings.data.content += ''; } $usb.ajax( requestId, settings ); }, /** * Controls the number of columns in a row * * @param {String} id Shortcode's usbid, e.g. "vc_row:1" * @param {String} layout The layout * @resurn {Boolean} Returns true if rendered, false otherwise. */ _updateColumnsLayout: function( rowId, layout ) { const self = this; // Exclusion of custom settings, since we do not change the rows, but only apply `--custom-columns` if ( 'custom' === layout ) { return; } var columns = self.getElmChildren( rowId ), columnsCount = columns.length, renderNeeded = false, columnType = self.isRow( rowId ) ? 'vc_column' : 'vc_column_inner', newColumnsWidths = [], newColumnsWidthsBase = 0, newColumnsWidthsTmp, newColumnsCount; // Make sure layout has the string type, so our checks will be performed right way layout = $ush.toString( layout ); // Parse layout value into columns array // Complex layout with all column widths specified if ( layout.includes( '-' ) ) { newColumnsWidthsTmp = layout.split( '-' ); newColumnsCount = newColumnsWidthsTmp.length; // Calculate columns width base for ( var i = 0; i < newColumnsCount; i ++ ) { newColumnsWidthsBase += $ush.parseInt( newColumnsWidthsTmp[ i ] ); } // Calculate and assign columns widths for ( var i = 0; i < newColumnsCount; i ++ ) { var columnWidthBaseTmp = newColumnsWidthsBase / newColumnsWidthsTmp[ i ]; // Try to transform width to a simple value (for example 2/4 will be transformed to 1/2) if ( columnWidthBaseTmp % 1 === 0 ) { newColumnsWidths.push( '1/' + columnWidthBaseTmp ); } else { newColumnsWidths.push( newColumnsWidthsTmp[ i ] + '/' + newColumnsWidthsBase ); } } // Layout with column that use grid-template-columns (space separated) } else if ( ! layout.includes( '(' ) && REGEXP_SPACE.test( layout ) ) { const customColumns = layout .trim() .split( REGEXP_SPACE ) .filter( ( value ) => /^(?:\d+(?:\.\d+)?(?:px|rem|em|%)|\d+fr|auto)$/.test( value ) ); newColumnsCount = 0; for ( var i in customColumns ) { newColumnsWidths.push( '1/1' ); newColumnsCount++; } // Simple layout with column number only } else { if ( $ush.isNumeric( layout ) ) { newColumnsCount = $ush.parseInt( layout ); } else { newColumnsCount = layout.split( REGEXP_SPACE ).length; } // limit maximum number of columns if ( newColumnsCount > 10 ) { newColumnsCount = 10; } for ( var i = 0; i < newColumnsCount; i ++ ) { newColumnsWidths.push( '1/' + layout ); } } // Add new columns if needed if ( columnsCount < newColumnsCount ) { for ( var i = columnsCount; i < newColumnsCount; i ++ ) { const newColumnId = self.getSpareElmId( columnType ); self.insertShortcodeIntoContent( rowId, i, `[${columnType} usbid="${newColumnId}"][/${columnType}]` ); elementsMap.set( newColumnId, { parentId: rowId, elmIndex: i }); } columnsCount = newColumnsCount; // Need to render newly added columns renderNeeded = true; // Trying to remove extra columns if needed (only empty columns may be removed) } else if ( columnsCount > newColumnsCount ) { var columnsCountDifference = columnsCount - newColumnsCount; for ( var i = columnsCount - 1; ( i >= 0 ) && ( columnsCountDifference > 0 ); i -- ) { var columnChildren = self.getElmChildren( columns[ i ] ); if ( columnChildren.length === 0 ) { self.removeElm( columns[ i ] ); columnsCountDifference--; } } columnsCount = newColumnsCount + columnsCountDifference; } // Refresh columns list columns = self.getElmChildren( rowId ); $usb.trigger( 'builder.contentChange' ); // Set new widths for columns for ( var i = 0; i < columnsCount; i ++ ) { self.setElmValues( columns[ i ], { width: newColumnsWidths[ i % newColumnsWidths.length ] } ); } return renderNeeded; }, /** * Get the insert position * * @param {String} parent Shortcode's usbid, e.g. "us_btn:1" or 'rootContainer' * @param {Number} index Position of the element inside the parent * @return {{position: String, parent: String}} Object with new data */ getInsertPosition: function( parent, index ) { const self = this; const parsedIndex = $ush.parseInt( index ); // Non-container elements always insert relative to themselves if ( ! self.isRootContainer( parent ) && ! self.isElmContainer( parent ) ) { return { position: parsedIndex < 1 ? 'before' : 'after', parent: parent }; } const children = self.getElmChildren( parent ); const childrenLength = children.length; // Empty container or inserting at the very beginning if ( childrenLength === 0 || parsedIndex <= 0 ) { return { position: 'prepend', parent: parent }; } // Inserting at or beyond the last element if ( parsedIndex >= childrenLength ) { return { position: 'append', parent: parent }; } // Inserting between existing elements return { position: 'after', parent: children[ parsedIndex - 1 ] }; }, /** * Add shortcode to a given position * * @param {String} parent Shortcode's usbid, e.g. "us_btn:1" * @param {Number} index Position of the element inside the parent * @param {String} newShortcode The new shortcode * @return {Boolean} True if successful, False otherwise */ insertShortcodeIntoContent: function( parent, index, newShortcode ) { const self = this; if ( ! newShortcode || ! ( self.isValidId( parent ) || self.isRootContainer( parent ) ) ) { return false; } const insertPosition = self.getInsertPosition( parent, index ); parent = insertPosition.parent; const isRootContainer = self.isRootContainer( parent ); const elmType = ! isRootContainer ? self.getElmType( parent ) : ''; const content = $ush.toString( self.pageData.content ); var oldShortcode = ! isRootContainer ? self.getElmShortcode( parent ) : content; // Remove html from start and end oldShortcode = self.removeHtmlWrap( oldShortcode ); // NOTE: The check runs on the stripped value, since that is what the replace() below // matches on. An empty anchor matches at offset 0, i.e. the shortcode would be inserted // at the very beginning of the page, outside of any container. if ( ! isRootContainer && ! oldShortcode ) { $usb.log( 'Error: The insertion anchor was not found in the content:', arguments ); return false; } // Check the position for the root element, if the position is before or after then add the element to the `prepend` var position = insertPosition.position; if ( isRootContainer ) { position = ( position === 'before' || position === 'after' ) ? 'container:prepend' : `container:${position}`; } // Create new shortcode var insertShortcode = ''; if ( position === 'before' || position === 'container:prepend' ) { insertShortcode = newShortcode + oldShortcode; } else if ( position === 'prepend' ) { insertShortcode = oldShortcode.replace( new RegExp( '^(\\['+ elmType +'.*?[\\^\\]]+)' ), ( _, match ) => { return match + newShortcode; } ); } else if ( position === 'append' && self.parseShortcode( oldShortcode ).hasClosingTag ) { insertShortcode = oldShortcode.replace( new RegExp( '(\\[\\/'+ elmType +'\])$' ), ( _, match ) => { return newShortcode + match; } ); // For "append:not(hasClosingTag)", after", "container:append" and default } else { insertShortcode = oldShortcode + newShortcode; } self.pageData.content = content.replace( oldShortcode, insertShortcode ); return true; }, /** * Get the default content * * NOTE: Get content by default has been moved to a separate method to unload and simplify methods * * @param {String} elmType The elm type * @return {String} The default content */ _getDefaultContent: function( elmType ) { const self = this; const shortcodeConfig = $usb.config( 'shortcode', {} ); const _getDefaultContent = ( type ) => { var defaultValues = ( shortcodeConfig.default_values || {} )[ type ] || false, editContent = ( shortcodeConfig.edit_content || {} )[ type ] || false; if ( editContent && defaultValues && defaultValues[ editContent ] ) { return defaultValues[ editContent ]; } return ''; }; // Defines and create a required child if needed const child = $usb.config( `shortcode.relations.as_parent.${elmType}.only` ); if ( ! child ) { return _getDefaultContent( elmType ); } // Add elements for tab structures if ( self.isElmSection( child ) ) { const titleTemplate = $usb.getTextTranslation( 'section' ), // Get parameters for a template params = { title_1: ( titleTemplate + ' 1' ), title_2: ( titleTemplate + ' 2' ), vc_column_text: self.getSpareElmId( 'vc_column_text' ), vc_column_text_content: _getDefaultContent( 'vc_column_text' ), vc_tta_section_1: self.getSpareElmId( child ), vc_tta_section_2: self.getSpareElmId( child ) }; // Build shortcode return $usb.buildString( $usb.config( `template.${child}`, '' ), params ); // Specify default content for timelines } else if ( child === 'timeline_section' ) { params = { // us_ prefix is important here, it is impossible to add a new element to just 'timeline_section', // as there would be wrong regex for parent timeline_section_usbid1: self.getSpareElmId( 'us_timeline_section' ), timeline_section_usbid2: self.getSpareElmId( 'us_timeline_section' ), timeline_section_usbid3: self.getSpareElmId( 'us_timeline_section' ), vc_column_text_usbid1: self.getSpareElmId( 'vc_column_text' ), vc_column_text_usbid2: self.getSpareElmId( 'vc_column_text' ), vc_column_text_usbid3: self.getSpareElmId( 'vc_column_text' ), us_text_usbid1: self.getSpareElmId( 'us_text' ), us_text_usbid2: self.getSpareElmId( 'us_text' ), us_text_usbid3: self.getSpareElmId( 'us_text' ), vc_column_text_content: _getDefaultContent( 'vc_column_text' ), }; return $usb.buildString( $usb.config( `template.timeline`, '' ), params ); // Add an empty element with no content } else { return '['+ child +' usbid="'+ self.getSpareElmId( child ) +'"][/'+ child +']'; } }, /** * Add a new element to the preview. * * @param {String} elmId The element ID * @param {Number} elmIndex The element index * @param {String} parentId The parent ID * @param {Function} callback The callback [optional] * @param {String} newTargetId [optional] */ addElmToPreview: function( elmId, elmIndex, parentId, callback, newTargetId ) { const self = this; if ( ! self.isValidId( elmId ) ) { return; } const insert = self.getInsertPosition( parentId, elmIndex ); $usb.postMessage( 'showPreloader', [ insert.parent, insert.position, // If these values are true, then a container class will be added for customization self.isElmContainer( self.getElmType( elmId ) ), newTargetId ] ); self.renderShortcode( `addElmToPreview[${newTargetId}]`, { data: { content: self.getElmShortcode( elmId ), }, success: ( res ) => { $usb.postMessage( 'hidePreloader', newTargetId || insert.parent ); if ( res.success ) { $usb.postMessage( 'insertElement', [ insert.parent, insert.position, res.data.html ] ); $usb.postMessage( 'maybeInitElmJS', [ elmId ] ); $usb.trigger( 'builder.contentChange' ); $usb.history.commitChange( elmId, ACTION_CONTENT.CREATE ); } if ( typeof callback === 'function' ) { callback.call( self, elmId ); } } } ); }, /** * Reloads the element in the preview by its ID. * * @param {String} elmId The element ID * @param {Function} callback The callback [optional] */ reloadElmInPreview: function( elmId, callback ) { const self = this; if ( ! self.isValidId( elmId ) || self.isRootContainer( elmId ) ) { return; } // The callback must run on every outcome, otherwise callers that chain their own work on // it (e.g. the history recovery queue) stall for good on a failed request. const runCallback = () => { if ( typeof callback === 'function' ) { callback.call( self, elmId ); } }; $usb.postMessage( 'showPreloader', elmId ); self.renderShortcode( 'reloadElmInPreview', { data: { content: self.getElmShortcode( elmId ), }, error: () => { $usb.postMessage( 'hidePreloader', elmId ); runCallback(); }, success: ( res ) => { $usb.postMessage( 'hidePreloader', elmId ); $usb.postMessage( 'doAction', [ 'removeHighlights', /*force*/true ] ); if ( res.success ) { var html = $ush .toString( res.data.html ) .replace( /(us_animate_this)/g, "$1 start" ); // Reload element in preview $usb.postMessage( 'updateSelectedElm', [ elmId, html ] ); } runCallback(); $usb.trigger( 'builder.contentChange' ); } } ); }, /** * Create and add a new element * * @param {String} type The element type * @param {String} parentId The parent id * @param {Number} elmIndex Position of the element inside the parentId * @param {{}} values The element values * @param {Function} callback The callback [optional] * @return {*} */ createElm: function( type, parentId, elmIndex, values, callback ) { const self = this; const args = arguments; const isRootContainer = self.isRootContainer( parentId ); if ( ! type || ! parentId || ! ( self.isValidId( parentId ) || isRootContainer ) ) { $usb.log( 'Error: Invalid params', args ); return; } // Check parents and prohibit invest in yourself if ( self.hasSameTypeParent( type, parentId ) ) { $usb.log( 'Error: It is forbidden to add inside the same type', args ); return; } $usb.postMessage( 'doAction', 'hideHighlight' ); // Index check and position determination elmIndex = $ush.parseInt( elmIndex ); // If there is no parent element, add the element to the `nainContainer` if ( ! isRootContainer && ! self.doesElmExist( parentId ) ) { parentId = self.rootContainer; elmIndex = 0; } var elmId = self.getSpareElmId( type ), elmName = self.getElmName( elmId ); if ( ! $.isPlainObject( values ) ) { values = {}; } var buildShortcode = self.buildShortcode({ tag: type, atts: self.buildAtts( $.extend( { usbid: elmId }, values ) ), content: self._getDefaultContent( elmName ), hasClosingTag: ( self.isElmContainer( elmName ) || !! $usb.config( `shortcode.edit_content.${elmName}` ) ) } ); // The check if the element is not the root container and is added to the root container, // then add a wrapper `vc_row`. It is forbidden to add elements without a line to the root container! if ( self.isRootContainer( parentId ) && ! self.isRemoveRows && ! self.isRow( elmId ) && ! $usb.templates.isTemplate( type ) ) { elmId = self.getSpareElmId( 'vc_row' ); buildShortcode = $usb.buildString( $usb.config( 'template.vc_row', '' ), { vc_row: elmId, vc_column: self.getSpareElmId( 'vc_column' ), content: buildShortcode } ); } if ( ! self.insertShortcodeIntoContent( parentId, elmIndex, buildShortcode ) ) { return false; } // Reload element in preview if ( self.isReloadElm( parentId ) ) { self.reloadElmInPreview( parentId ); $usb.history.commitChange( elmId, ACTION_CONTENT.CREATE ); // Add new element to preview } else { self.addElmToPreview( elmId, elmIndex, parentId, callback ); } self.reloadElementsMap(); return elmId; }, /** * Move the element to a new position * * @param {String} moveId ID of the element that is being moved, e.g. "us_btn:1" * @param {String} newParentId ID of the element's new parent element * @param {Number} newIndex Position of the element inside the new parent * @return {Boolean} */ moveElm: function( moveId, newParentId, newIndex ) { const self = this; const args = arguments; // NOTE: The moved element may be excluded from the elements map by the drag machinery // (navigator maybeDrag / preview _maybeDrag). All index math below MUST run in these // "map without the moved element" coordinates — drag sources compute insertIndex the // same way, and getInsertPosition() must never pick the moved element itself as the // insert anchor (its shortcode is already cut from the content at that point). // The map is restored via includeExcludedElements() right after the content insert. // The moved element is excluded from the map by the caller BEFORE moveElm runs, so any // early return below would strand it outside elementsMap (it vanishes from the navigator // until a full reload). The finally guarantees restoration on EVERY exit path; the // success path restores explicitly earlier so the code after it sees the element in the // map (and the finally then becomes a guarded no-op). try { if ( self.isRootContainer( moveId ) ) { $usb.log( 'Error: Cannot move the container', args ); return false; } const isRootContainer = self.isRootContainer( newParentId ); // Check parents and prohibit invest in yourself if ( self.hasSameTypeParent( moveId, newParentId ) ) { $usb.log( 'Error: It is forbidden to add inside the same type', args ); return; } // Check the correctness of ids if ( ! self.isValidId( moveId ) || ! ( self.isValidId( newParentId ) || isRootContainer ) ) { $usb.log( 'Error: Invalid ID specified', args ); return false; } // Remove the element from the map for correct undo/redo index math self.excludeElement( moveId ); // The moved element is out of the map, so fall back to its stashed // entry so the old-parent reload below keeps working (see excludeElement) const oldParentId = self.getElmParentId( moveId ) || ( self.getExcludedElement( moveId ) || {} ).parentId || null; newIndex = $ush.parseInt( newIndex ); $usb.postMessage( 'doAction', 'hideHighlight' ); // If there is no newParentId element, add the element to the `rootContainer` if ( ! isRootContainer && ! self.doesElmExist( newParentId ) ) { newParentId = self.rootContainer; newIndex = 0; } // Get old shortcode and remove in content const oldContent = $ush.toString( self.pageData.content ); const oldShortcode = self.getElmShortcode( moveId ); self.pageData.content = oldContent.replace( oldShortcode, '' ); const insert = self.getInsertPosition( newParentId, newIndex ); // Added shortcode to content if ( ! self.insertShortcodeIntoContent( newParentId, newIndex, oldShortcode ) ) { // The shortcode is already cut out, so a failed insert would lose the element self.pageData.content = oldContent; return false; } // Commit to save changes to history // NOTE: Committed only once the content has actually changed. An earlier commit would // leave a no-op MOVE in the undo stack — and wipe the whole redo stack — whenever the // insert above fails. It reads the element's original position from the exclusion // stash (see commitChange), so it must stay ABOVE includeExcludedElements(). $usb.history.commitChange( moveId, ACTION_CONTENT.MOVE ); // The index math is done — restore the excluded element so consumers of the map see it: // the preview's moveElm handler needs getElmParentId( moveId ) to sync the tab-button // order in .w-tabs-list when moving sections of Tabs/Tour/Accordion. self.includeExcludedElements(); $usb.postMessage( 'moveElm', [ insert.parent, insert.position, moveId ] ); // Reload element in preview if ( self.isReloadElm( oldParentId ) ) { self.reloadElmInPreview( oldParentId ); } else if ( self.isReloadElm( newParentId ) ) { self.reloadElmInPreview( newParentId ); }; $ush.timeout( () => { self.reloadElementsMap().then( () => $usb.trigger( 'builder.contentChange' ) ); } ); return true; } finally { // Safety net: restore the moved element on the early-return/error paths above. self.includeExcludedElements(); } }, /** * Remove the element * * @param {String} removeId ID of the element that is being removed, e.g. "us_btn:1" * @return {Boolean} */ removeElm: function( removeId ) { const self = this; if ( ! self.isValidId( removeId ) ) { return false; } $usb.postMessage( 'removeHtmlById', removeId ); const selectedElmId = self.selectedElmId; const rootContainerId = self.getElmParentId( removeId ); var children = self.getElmAllChildren( removeId ); $usb.history.commitChange( removeId, ACTION_CONTENT.REMOVE ); self.pageData.content = $ush .toString( self.pageData.content ) .replace( self.getElmShortcode( removeId ), '' ); $usb.trigger( 'builder.contentChange' ); if ( self.isColumn( removeId ) ) { $usb.postMessage( 'vcColumnChanged', /*row|row_inner id*/rootContainerId ); } // Reload element in preview if ( self.isReloadElm( rootContainerId ) ) { self.reloadElmInPreview( rootContainerId ); } if ( selectedElmId && ( removeId == selectedElmId // for current element || children.includes( selectedElmId ) // for parent element ) ) { $usb.trigger( 'panel.showElementsSection' ); } children.push( removeId ); for ( const k in children ) { elementsMap.delete( children[ k ] ); } $usb.navigator.removeElm( removeId ); return true; }, /** * Update IDs in content. * * @param {String} content The shortcode content. * @param {String} html [optional] * @return {{}} */ updateIdsInContent: function( content, html ) { const self = this; var firstElmId, // first shortcode usbid (should be a vc_row) customPrefix = $usb.config( 'designOptions.customPrefix', 'usb_custom_' ); html = $ush.toString( html ); content = $ush.toString( content ); // Replace all usbid's in content and html content = content.replace( REGEXP_USBID, ( match, input, elmId ) => { // Get a new usbid of the same type const newElmId = self.getSpareElmId( elmId ); if ( ! firstElmId ) { firstElmId = newElmId; // get first shortcode usbid (should be a vc_row) } if ( html ) { html = html // Replace all usbid's in attributes .replace( new RegExp( `data-(for|usbid)="${elmId}"`, 'g' ), `data-$1="${newElmId}"` ) // Replace all custom element classes, old mask: `{customPrefix}{type}{index}` .replace( new RegExp( customPrefix + elmId.replace( ':', '' ), 'g' ), $ush.uniqid( customPrefix ) ); } return input.replace( elmId, newElmId ); } ); return { firstElmId: firstElmId, content: content, html: html, }; }, } ); $usb.builder = new Builder(); } ( jQuery );