diff --git a/interactions.php b/interactions.php index 9ad3c45..2e31575 100644 --- a/interactions.php +++ b/interactions.php @@ -81,13 +81,12 @@ function interact_on_activation() { require_once( plugin_dir_path( __FILE__ ) . 'src/editor/editor.php' ); } else { add_action( 'after_setup_theme', function() { - if ( - ( function_exists( 'bricks_is_builder_main' ) && bricks_is_builder_main() ) || - ( function_exists( 'bricks_is_builder' ) && bricks_is_builder() ) - ) { + if ( function_exists( 'et_core_is_fb_enabled' ) || + function_exists( 'bricks_is_builder_main' ) || + function_exists( 'bricks_is_builder' ) ) { require_once( plugin_dir_path( __FILE__ ) . 'src/editor/editor.php' ); } - } ); + }, 20 ); } /** diff --git a/src/editor/app.js b/src/editor/app.js index f0b0598..da200b8 100644 --- a/src/editor/app.js +++ b/src/editor/app.js @@ -1,5 +1,6 @@ import ElementSVG from './assets/element.svg' import PageSVG from './assets/page.svg' +import LibrarySVG from './assets/library.svg' import { AddInteractionButton, InteractionButton, @@ -8,6 +9,10 @@ import { } from './components' import { createNewInteraction, createNewAction } from './util' import { useInteractions } from './hooks' +import { + getCurrentSelectedTarget, + isBuilderEditor, +} from './editors' import { interactions as interactionsConfig, manageInteractionsUrl } from 'interactions' import { __ } from '@wordpress/i18n' @@ -74,6 +79,7 @@ const InteractionsApp = ( { // Interaction library open modal and set target function. const { setMode: setInteractionLibraryMode, + setTarget: setInteractionLibraryTarget, } = useDispatch( 'interact/interaction-library-modal' ) const [ selectedInteraction, setSelectedInteraction ] = useState( null ) @@ -208,6 +214,23 @@ const InteractionsApp = ( { setImportExportModalProps( null ) } + const onOpenInteractionLibraryHandler = () => { + const selectedTarget = getCurrentSelectedTarget() + + if ( ! selectedTarget ) { + alert( __( 'Select an element in the editor first before opening the Interaction Library.', 'interactions' ) ) // eslint-disable-line no-alert + return + } + + setInteractionLibraryTarget( selectedTarget ) + setInteractionLibraryMode( 'apply' ) + } + + const onOpenInteractionLibraryInsertHandler = () => { + setInteractionLibraryTarget( null ) + setInteractionLibraryMode( 'insert' ) + } + return <> { selectedInteraction === null && loadingError && isShowingError && @@ -236,6 +259,29 @@ const InteractionsApp = ( { } + { isBuilderEditor() && selectedInteraction === null && + + +
+ + +
+
+
+ } { allInteractions.length > 0 && selectedInteraction === null && { interactions.length > 0 &&

{ __( 'These interactions are on this page because of their location rules.', 'interactions' ) }

} diff --git a/src/editor/components/interaction-panel/index.js b/src/editor/components/interaction-panel/index.js index 79688be..546618d 100644 --- a/src/editor/components/interaction-panel/index.js +++ b/src/editor/components/interaction-panel/index.js @@ -23,6 +23,7 @@ import { useLayoutEffect, } from '@wordpress/element' import { Icon, download } from '@wordpress/icons' +import { saveCurrentEditor } from '~interact/editor/editors' import TargetSelector from '../target-selector' import { getInteractionWarning } from './util' @@ -225,11 +226,13 @@ const InteractionPanel = props => { setStatus( 'publishing' ) // TODO: if publishing and then we are missing a target, we should show a notice. onChange( editedInteraction ).then( () => { - setStatus( 'idle' ) - setIsDirty( false ) - if ( callback ) { - setTimeout( callback, 1 ) // Need a timeout here because re-publishing may be too fast. - } + return Promise.resolve( saveCurrentEditor() ).finally( () => { + setStatus( 'idle' ) + setIsDirty( false ) + if ( callback ) { + setTimeout( callback, 1 ) // Need a timeout here because re-publishing may be too fast. + } + } ) } ) }, [ editedInteraction, onChange ] ) diff --git a/src/editor/components/target-selector/index.js b/src/editor/components/target-selector/index.js index 1104936..7dc5998 100644 --- a/src/editor/components/target-selector/index.js +++ b/src/editor/components/target-selector/index.js @@ -4,6 +4,7 @@ import { GridLayout, FlexLayout } from '~interact/editor/components' import { getSelectedBlockAnchor, isBricksEditor, + isDiviEditor, isElementorEditor, startEditorElementPicker, } from '~interact/editor/editors' @@ -43,7 +44,7 @@ const TargetSelector = props => { noArrow = false, } = props - const isBuilder = isBricksEditor() || isElementorEditor() + const isBuilder = isBricksEditor() || isElementorEditor() || isDiviEditor() const isElementor = isElementorEditor() const hasBlockEditor = !! select( 'core/block-editor' )?.getSelectedBlockClientId const [ isPopoverOpen, setIsPopoverOpen ] = useState( false ) @@ -59,6 +60,8 @@ const TargetSelector = props => { const displayType = isElementor && elementorUiType === 'elementor-element' ? 'elementor-element' : value.type + // Remove the picker button for Divi when the target type is class, since Divi doesn't support it. + const isDiviManualClassInput = isDiviEditor() && displayType === 'class' const targetButton = ( <> @@ -219,6 +222,13 @@ const TargetSelector = props => { targetOptions = targetOptions.filter( target => bricksTargetTypes.includes( target.value ) ) } + if ( isDiviEditor() ) { + const diviTargetOrder = [ 'selector', 'class', 'trigger', 'window' ] + targetOptions = diviTargetOrder + .map( targetValue => targetOptions.find( target => target.value === targetValue ) ) + .filter( Boolean ) + } + useEffect( () => { return () => { elementPickerStopRef.current?.() @@ -313,7 +323,7 @@ const TargetSelector = props => { ) } { displayType === 'class' && ( - { isHorizontal && targetButton } + { isHorizontal && ! isDiviManualClassInput && targetButton } { } } } /> - { ! isHorizontal && targetButton } + { ! isHorizontal && ! isDiviManualClassInput && targetButton } ) } { displayType === 'block-name' && ( diff --git a/src/editor/editor.php b/src/editor/editor.php index 918cbb8..9744ed9 100644 --- a/src/editor/editor.php +++ b/src/editor/editor.php @@ -23,6 +23,12 @@ function __construct() { } add_action( 'elementor/editor/after_enqueue_scripts', array( $this, 'enqueue_elementor_editor' ) ); + // Only register the Divi builder enqueue callback when Divi is + // present on the request. + if ( function_exists( 'et_core_is_fb_enabled' ) ) { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_divi_editor' ) ); + } + // Only register the Bricks builder enqueue callback when Bricks is // present on the request. if ( function_exists( 'bricks_is_builder_main' ) || function_exists( 'bricks_is_builder' ) ) { @@ -70,6 +76,29 @@ public function enqueue_bricks_editor() { $this->enqueue_editor( 'bricks' ); } + /** + * Loads the editor script inside Divi's Visual Builder top window. + * + * @return void + */ + public function enqueue_divi_editor() { + $is_divi_builder = function_exists( 'et_core_is_fb_enabled' ) && + et_core_is_fb_enabled() && + ( ! isset( $_GET['app_window'] ) || '1' !== $_GET['app_window'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( ! $is_divi_builder ) { + return; + } + + // Defense-in-depth: only load the builder editor for users who can + // edit content, even if the request matches Divi's builder URL. + if ( ! current_user_can( 'edit_posts' ) ) { + return; + } + + $this->enqueue_editor( 'divi' ); + } + /** * Loads the editor script. * diff --git a/src/editor/editor.scss b/src/editor/editor.scss index 67a212b..ab1d07e 100644 --- a/src/editor/editor.scss +++ b/src/editor/editor.scss @@ -142,7 +142,8 @@ /* Interaction Elementor Editor Panel Styles */ .interact-elementor-launcher, -.interact-bricks-launcher { +.interact-bricks-launcher, +.interact-divi-launcher { display: inline-flex; align-items: center; justify-content: flex-start; @@ -181,7 +182,8 @@ } .interact-elementor-launcher.is-hidden, -.interact-bricks-launcher.is-hidden { +.interact-bricks-launcher.is-hidden, +.interact-divi-launcher.is-hidden { opacity: 0; pointer-events: none; } @@ -230,6 +232,11 @@ } } +.interact-divi-panel { + top: 0; + height: 100vh; +} + .interact-pagebuilder-panel.is-open { transform: translateX(0); } diff --git a/src/editor/editors/abstract.js b/src/editor/editors/abstract.js index 5c97d21..c2a7273 100644 --- a/src/editor/editors/abstract.js +++ b/src/editor/editors/abstract.js @@ -6,6 +6,8 @@ import { pluginVersion, srcUrl, } from 'interactions' +import { applyTargetMappings } from '../interaction-library/util' +import { getPresetBuilderTargetRefs } from '../interaction-library/preset-schema' import { select } from '@wordpress/data' const NOOP = () => {} @@ -36,12 +38,20 @@ class InteractionsEditorAbstract { return this.getEditorMode() === 'bricks' } + isDivi() { + return this.getEditorMode() === 'divi' + } + isGutenberg() { return this.getEditorMode() === 'gutenberg' } isBuilder() { - return this.isElementor() || this.isBricks() + return this.isElementor() || this.isBricks() || this.isDivi() + } + + getPresetTargetRefs( selectedPreset = {} ) { + return getPresetBuilderTargetRefs( selectedPreset, this.getEditorMode() ) } ensureBuilderEditorStyles() { @@ -111,6 +121,47 @@ class InteractionsEditorAbstract { return NOOP } + canInsertPreset() { + return false + } + + // Persist the parent editor when the interaction data should also be saved. + saveEditor() { + return Promise.resolve() + } + + insertPresetContent() { + return null + } + + // Insert a library preset into the current editor and return the inserted + // content descriptor so target mappings can be resolved afterward. + insertLibraryPreset( selectedPreset ) { + return this.insertPresetContent( selectedPreset ) + } + + // Resolve a preset's target mappings against either inserted editor content + // or a selected target object, depending on the current library mode. + resolveLibraryPresetTargets( interactionSetup, selectedPreset = {}, insertionContext = null ) { + const targetMappingsSource = insertionContext?.targetMappingsSource ?? insertionContext + const fallbackTarget = insertionContext?.defaultTarget ?? targetMappingsSource + const targetRefs = insertionContext?.targetRefs ?? this.getPresetTargetRefs( selectedPreset ) + const resolver = insertionContext?.resolveTargetMappingTarget ?? null + + applyTargetMappings( + interactionSetup, + selectedPreset.targetMappings, + Array.isArray( selectedPreset.targetMappings ) && selectedPreset.targetMappings.length > 0 + ? targetMappingsSource + : fallbackTarget, + [ 'target' ], + targetRefs, + resolver + ) + + return interactionSetup + } + // Start an editor-specific target picker. startElementPicker( args = {} ) { const { diff --git a/src/editor/editors/bricks.js b/src/editor/editors/bricks.js index 3efef1d..5b40d75 100644 --- a/src/editor/editors/bricks.js +++ b/src/editor/editors/bricks.js @@ -1,6 +1,12 @@ import IconSVG from '../assets/icon.svg' import InteractionsApp from '../app' import InteractionsEditorAbstract from './abstract' +import { InteractionLibraryRoot } from '../interaction-library' +import { cloneBricksExample } from '../interaction-library/bricks-example' +import { + getPresetBuilderExample, + getPresetBuilderTargetRefs, +} from '../interaction-library/preset-schema' import { __ } from '@wordpress/i18n' import { Button } from '@wordpress/components' @@ -9,6 +15,7 @@ import { useState, createRoot, } from '@wordpress/element' +import { currentPostId } from 'interactions' const NOOP = () => {} @@ -90,6 +97,7 @@ class BricksInteractionsEditor extends InteractionsEditorAbstract { + ) } @@ -121,6 +129,10 @@ class BricksInteractionsEditor extends InteractionsEditorAbstract { return '' } + if ( typeof element.id === 'string' && element.id.startsWith( 'brxe-' ) ) { + return element.id.replace( /^brxe-/, '' ) + } + if ( element.dataset?.id ) { return element.dataset.id } @@ -161,6 +173,235 @@ class BricksInteractionsEditor extends InteractionsEditorAbstract { return this.buildTargetFromElement( this.selectedElement.element ) } + // Read the localized Bricks builder payload from the main window. + getBricksBootData() { + const dataScript = document.querySelector( '#bricks-builder-js-extra' ) + const scriptContent = dataScript?.textContent || '' + const match = scriptContent.match( /var\s+bricksData\s*=\s*(\{[\s\S]*\});/ ) + + if ( ! match ) { + return null + } + + try { + return JSON.parse( match[ 1 ] ) + } catch ( error ) { + return null + } + } + + // Resolve the current Bricks builder post ID with a localized fallback. + getBuilderPostId() { + const bootData = this.getBricksBootData() + return Number( bootData?.postId ) || Number( currentPostId ) || 0 + } + + // Map the active Bricks template type to the saved data area we need. + getBuilderArea() { + const templateType = this.getBricksBootData()?.loadData?.templateType || 'content' + return [ 'header', 'footer' ].includes( templateType ) ? templateType : 'content' + } + + // Run a Bricks AJAX action using the same nonce and endpoint as the builder. + async runBricksAjaxAction( action, data = {} ) { + const bootData = this.getBricksBootData() + const ajaxUrl = bootData?.ajaxUrl + const nonce = bootData?.nonce + + if ( ! ajaxUrl || ! nonce ) { + return null + } + + const formData = new window.FormData() + formData.append( 'action', action ) + formData.append( 'nonce', nonce ) + + Object.entries( data ).forEach( ( [ key, value ] ) => { + if ( value === undefined || value === null ) { + return + } + + formData.append( + key, + typeof value === 'string' ? value : JSON.stringify( value ) + ) + } ) + + const response = await window.fetch( ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + body: formData, + } ) + + return response.json() + } + + // Load the current saved Bricks elements for the active builder area. + async getBuilderElements( postId, area ) { + const response = await this.runBricksAjaxAction( 'bricks_get_partial_builder_data', { + postId, + } ) + const elements = response?.success ? response?.data?.[ area ] : null + return Array.isArray( elements ) ? elements : [] + } + + // Persist a full Bricks element array through the builder save endpoint. + async saveBuilderElements( postId, area, elements ) { + const templateType = this.getBricksBootData()?.loadData?.templateType || 'content' + + return this.runBricksAjaxAction( 'bricks_save_post', { + postId, + templateType, + [ area ]: elements, + } ) + } + + // Ask Bricks to render the updated builder area so the preview can refresh in place. + async renderBuilderElements( postId, area, elements ) { + return this.runBricksAjaxAction( 'bricks_render_data', { + postId, + area, + elements, + 'bricks-is-builder': 1, + } ) + } + + // Replace the preview markup with freshly rendered Bricks HTML after a preset insert. + async refreshBuilderPreview( postId, area, elements ) { + const previewDocument = this.getCanvasDocument() + const previewRoot = previewDocument?.querySelector( '#brx-content' ) + const bootData = window.bricksData || this.getBricksBootData() + + if ( bootData?.loadData ) { + bootData.loadData[ area ] = structuredClone( elements ) + } + + if ( ! previewDocument || ! previewRoot ) { + return + } + + const response = await this.renderBuilderElements( postId, area, elements ) + const renderedHtml = response?.success ? response?.data?.html : '' + + if ( ! renderedHtml ) { + return + } + + previewRoot.innerHTML = renderedHtml + } + + // Merge inserted preset elements into the current Bricks flat element list. + insertBricksElements( currentElements = [], insertedElements = [], rootElementIds = [] ) { + const nextElements = currentElements.map( element => structuredClone( element ) ) + const selectedElementId = this.getSelectedElementId( this.selectedElement?.element || null ) + const insertedRoots = insertedElements.filter( element => rootElementIds.includes( element.id ) ) + const selectedElement = selectedElementId + ? nextElements.find( element => element.id === selectedElementId ) + : null + + if ( selectedElement && Array.isArray( selectedElement.children ) ) { + selectedElement.children = [ + ...selectedElement.children, + ...rootElementIds, + ] + insertedRoots.forEach( element => { + element.parent = selectedElement.id + } ) + } else if ( selectedElement?.parent ) { + const parentElement = nextElements.find( element => element.id === selectedElement.parent ) + + if ( parentElement && Array.isArray( parentElement.children ) ) { + const selectedIndex = parentElement.children.indexOf( selectedElement.id ) + if ( selectedIndex === -1 ) { + parentElement.children.push( ...rootElementIds ) + } else { + parentElement.children.splice( selectedIndex + 1, 0, ...rootElementIds ) + } + insertedRoots.forEach( element => { + element.parent = parentElement.id + } ) + } else { + nextElements.push( ...insertedElements ) + return nextElements + } + } else if ( selectedElement ) { + const selectedIndex = nextElements.findIndex( element => element.id === selectedElement.id ) + nextElements.splice( selectedIndex + 1, 0, ...insertedElements ) + return nextElements + } + + nextElements.push( ...insertedElements ) + return nextElements + } + + // Resolve a Bricks target ref by mapping a preset source ID to the new ID. + resolveInsertedTargetMapping( mapping = {}, inserted, bricksTargetRefs = {} ) { + const targetRefConfig = bricksTargetRefs?.[ mapping.targetRef ] + const sourceId = targetRefConfig?.id + if ( ! sourceId ) { + return null + } + + const insertedElementId = inserted?.sourceIdToInsertedId?.[ sourceId ] + if ( ! insertedElementId ) { + return null + } + + return this.buildTargetFromElement( { + id: `brxe-${ insertedElementId }`, + } ) + } + + canInsertPreset( preset ) { + const example = getPresetBuilderExample( preset, 'bricks' ) + return Array.isArray( example ) ? example.length > 0 : !! example + } + + // Insert a preset into Bricks by merging it into the saved builder data. + async insertPresetContent( preset ) { + if ( ! this.canInsertPreset( preset ) ) { + return null + } + + const postId = this.getBuilderPostId() + const area = this.getBuilderArea() + if ( ! postId ) { + return null + } + + const currentElements = await this.getBuilderElements( postId, area ) + const inserted = cloneBricksExample( getPresetBuilderExample( preset, 'bricks' ) ) + const mergedElements = this.insertBricksElements( + currentElements, + inserted.elements, + inserted.rootElementIds + ) + + const response = await this.saveBuilderElements( postId, area, mergedElements ) + if ( ! response?.success ) { + return null + } + + await this.refreshBuilderPreview( postId, area, mergedElements ) + + const firstInsertedRootId = inserted.rootElementIds[ 0 ] + const defaultTarget = firstInsertedRootId + ? this.buildTargetFromElement( { id: `brxe-${ firstInsertedRootId }` } ) + : null + const targetRefs = getPresetBuilderTargetRefs( preset, 'bricks' ) + + return { + targetMappingsSource: inserted, + resolveTargetMappingTarget: mapping => this.resolveInsertedTargetMapping( + mapping, + inserted, + targetRefs + ) || defaultTarget, + defaultTarget, + targetRefs, + } + } + registerSelectionTracking() { let isBound = false let observer = null diff --git a/src/editor/editors/divi.js b/src/editor/editors/divi.js new file mode 100644 index 0000000..480dca8 --- /dev/null +++ b/src/editor/editors/divi.js @@ -0,0 +1,1046 @@ +import IconSVG from '../assets/icon.svg' +import InteractionsApp from '../app' +import InteractionsEditorAbstract from './abstract' +import { InteractionLibraryRoot } from '../interaction-library' +import { + getPresetBuilderExample, + getPresetBuilderTargetRefs, +} from '../interaction-library/preset-schema' + +import { __ } from '@wordpress/i18n' +import { Button } from '@wordpress/components' +import { + createRoot, + useEffect, + useState, +} from '@wordpress/element' +import { customAlphabet } from 'nanoid' + +const NOOP = () => {} +const generateInteractionTargetId = customAlphabet( '1234567890abcdef', 10 ) +const DIVI_CONTAINER_MODULE_NAMES = [ + 'divi/root', + 'divi/section', + 'divi/row', + 'divi/row-inner', + 'divi/column', + 'divi/column-inner', + 'divi/group', + 'divi/group-carousel', +] +const DIVI_DIRECT_INSERT_MODULE_NAMES = [ + 'divi/root', + 'divi/column', + 'divi/column-inner', + 'divi/group', + 'divi/group-carousel', +] + +// Read a nested value from a plain object using an array path. +const getValueAtPath = ( object, path = [], defaultValue ) => { + let currentValue = object + + for ( const key of path ) { + if ( currentValue === null || typeof currentValue !== 'object' || ! ( key in currentValue ) ) { + return defaultValue + } + + currentValue = currentValue[ key ] + } + + return currentValue === undefined ? defaultValue : currentValue +} + +// Clone a plain object/array tree and replace one nested path. +const setValueAtPath = ( object, path = [], value ) => { + if ( path.length === 0 ) { + return value + } + + const [ key, ...restPath ] = path + const currentBranch = + object !== null && typeof object === 'object' + ? object + : ( typeof key === 'number' ? [] : {} ) + const nextBranch = setValueAtPath( currentBranch?.[ key ], restPath, value ) + + if ( Array.isArray( currentBranch ) ) { + const clonedArray = [ ...currentBranch ] + clonedArray[ key ] = nextBranch + return clonedArray + } + + return { + ...currentBranch, + [ key ]: nextBranch, + } +} + +// Mimic the small getIn/setIn API Divi's copy reducer expects from moduleObjects. +const createDiviModuleObjects = moduleObjects => { + const wrappedObjects = { ...moduleObjects } + + Object.defineProperties( wrappedObjects, { + getIn: { + enumerable: false, + value: ( path, defaultValue ) => getValueAtPath( wrappedObjects, path, defaultValue ), + }, + setIn: { + enumerable: false, + value: ( path, value ) => createDiviModuleObjects( setValueAtPath( wrappedObjects, path, value ) ), + }, + } ) + + return wrappedObjects +} + +// Normalize a Divi preset tree into the flat module map used by Divi content state. +const buildDiviPresetPayload = nodes => { + const parentId = 'interact-preset-root' + const moduleObjects = { + [ parentId ]: { + id: parentId, + name: 'divi/root', + parent: '', + children: [], + props: { + attrs: {}, + }, + }, + } + const rootIds = [] + + const registerNode = ( node, currentParentId ) => { + if ( ! node?.name ) { + return null + } + + const nodeId = node.id || `interact-${ generateInteractionTargetId() }` + moduleObjects[ nodeId ] = { + id: nodeId, + name: node.name, + parent: currentParentId, + children: [], + props: { + attrs: node.props || {}, + }, + } + moduleObjects[ currentParentId ].children.push( nodeId ) + + for ( const childNode of node.children || [] ) { + registerNode( childNode, nodeId ) + } + + return nodeId + } + + for ( const node of nodes ) { + const rootId = registerNode( node, parentId ) + if ( rootId ) { + rootIds.push( rootId ) + } + } + + return { + parentId, + rootIds, + moduleObjects: createDiviModuleObjects( moduleObjects ), + } +} + +// Convert Divi immutable/plain child collections into a simple array. +const toArray = value => { + if ( Array.isArray( value ) ) { + return value + } + + if ( value?.asMutable ) { + return value.asMutable( { deep: false } ) + } + + return [] +} + +// Wait for Divi to reflect a newly inserted child in the content tree. +const waitForDiviInsertedModule = async ( { + editPostSelect, + layout = '', + targetParentId = '', + insertTarget = {}, + previousChildren = [], + timeoutMs = 2000, + intervalMs = 50, +} ) => { + const startedAt = Date.now() + + while ( Date.now() - startedAt < timeoutMs ) { + const content = editPostSelect?.getContent?.( layout || undefined ) + const parentChildren = toArray( + content?.getIn?.( [ targetParentId, 'children' ], [] ) + ) + const insertedChildren = parentChildren.filter( + childId => ! previousChildren.includes( childId ) + ) + const insertedId = + insertedChildren[ 0 ] || + editPostSelect?.getNewlyInsertedModuleId?.( { + content, + id: insertTarget.anchorId, + position: insertTarget.position, + } ) + + if ( insertedId ) { + return { + insertedId, + content, + parentChildren, + insertedChildren, + } + } + + await new Promise( resolve => window.setTimeout( resolve, intervalMs ) ) + } + + return { + insertedId: null, + content: editPostSelect?.getContent?.( layout || undefined ) || null, + parentChildren: [], + insertedChildren: [], + } +} + +// Divi editor adapter for the initial Visual Builder integration milestone. +class DiviInteractionsEditor extends InteractionsEditorAbstract { + constructor() { + super() + this.selectedElement = null + this.selectionTrackingCleanup = null + } + + getEditorMode() { + return 'divi' + } + + // Mount the Divi launcher and builder panel shell in the top window only. + init() { + if ( this.initialized ) { + return this + } + + // Divi loads the page canvas in a separate app window iframe. Keep the + // Interactions shell in the top window so it mounts only once. + if ( window.frameElement ) { + return super.init() + } + + const mountNodeId = 'interact-divi-root' + if ( document.getElementById( mountNodeId ) ) { + return super.init() + } + + const editor = this + const DiviInteractionsEditorComponent = () => { + const [ isOpen, setIsOpen ] = useState( false ) + + const openPanel = () => { + editor.ensureBuilderEditorStyles().then( () => { + setIsOpen( true ) + } ) + } + + useEffect( () => { + const openHandler = () => openPanel() + window.addEventListener( 'interact/open-divi-sidebar', openHandler ) + return () => window.removeEventListener( 'interact/open-divi-sidebar', openHandler ) + }, [] ) + + return ( + <> + +
+
+
+ + { __( 'Interactions', 'interactions' ) } +
+
+
+
+ +
+
+
+ + + ) + } + + const mountNode = document.createElement( 'div' ) + mountNode.id = mountNodeId + mountNode.className = 'interact-builder-root interact-divi-root' + document.body.appendChild( mountNode ) + document.body.classList.add( 'interact-builder-editor' ) + document.body.classList.add( 'interact-divi-editor' ) + this.registerSelectionTracking() + createRoot( mountNode ).render( ) + + return super.init() + } + + openPanel() { + window.dispatchEvent( new CustomEvent( 'interact/open-divi-sidebar' ) ) + return null + } + + saveEditor() { + // Reuse Divi's own save button so the builder persists the current page + // after an interaction modifies module attributes such as target IDs. + const saveButton = Array.from( document.querySelectorAll( '.et-vb-page-bar-action-button' ) ) + .find( button => button.textContent?.trim() === 'Save' ) + + if ( ! saveButton || saveButton.disabled ) { + return Promise.resolve() + } + + saveButton.click() + return Promise.resolve() + } + + getCanvasDocument() { + const iframe = document.querySelector( 'iframe[src*="app_window=1"]' ) + return iframe?.contentDocument || null + } + + getCanvasWindow() { + const iframe = document.querySelector( 'iframe[src*="app_window=1"]' ) + return iframe?.contentWindow || null + } + + getDiviDataApi() { + const canvasWindow = this.getCanvasWindow() + + // Prefer the builder iframe first because Divi mounts most of its runtime + // there, then fall back to any mirrored top-window stores. + return ( + canvasWindow?.wp?.data || + canvasWindow?.divi?.data || + window.top?.wp?.data || + window.top?.divi?.data || + window.wp?.data || + window.divi?.data || + null + ) + } + + getModuleIdFromElement( element ) { + const targetElement = this.getSelectableElement( element ) + if ( ! targetElement ) { + return '' + } + + // Divi exposes the module identity on a few different attributes depending + // on the element type, so check the common variants in one place. + const moduleId = + targetElement.getAttribute( 'data-id' ) || + targetElement.getAttribute( 'data-wrapper-id' ) || + targetElement.getAttribute( 'data-module-id' ) || + targetElement.dataset?.id || + targetElement.dataset?.wrapperId || + targetElement.dataset?.moduleId || + '' + + return typeof moduleId === 'string' ? moduleId : '' + } + + getStoredInteractionTarget( moduleId ) { + if ( ! moduleId ) { + return '' + } + + const attrs = this.getDiviDataApi()?.select?.( 'divi/edit-post' )?.getModuleAttrs?.( moduleId ) + + // The store can return either Immutable-style values or plain objects, so + // support both shapes and normalize them to a simple string. + const interactionTarget = attrs?.getIn?.( + [ 'module', 'decoration', 'interactionTarget' ], + '' + ) ?? attrs?.module?.decoration?.interactionTarget ?? '' + + if ( interactionTarget && typeof interactionTarget === 'object' ) { + return interactionTarget.value || '' + } + + return typeof interactionTarget === 'string' ? interactionTarget : '' + } + + ensureInteractionTarget( moduleId ) { + if ( ! moduleId ) { + return '' + } + + const existingTarget = this.getStoredInteractionTarget( moduleId ) + if ( existingTarget ) { + return existingTarget + } + + // Persist the target on the Divi module itself so the same identifier is + // rendered in both the builder and the frontend output. + const targetId = generateInteractionTargetId( 10 ) + this.getDiviDataApi()?.dispatch?.( 'divi/edit-post' )?.editModuleAttribute?.( { + id: moduleId, + attrName: 'module.decoration.interactionTarget', + value: targetId, + caller: 'user', + subName: false, + } ) + return targetId + } + + getSelectableElement( element ) { + return element?.closest?.( '.et_pb_module, .et_pb_column, .et_pb_column_inner, .et_pb_row, .et_pb_row_inner, .et_pb_section' ) || null + } + + // Resolve the nearest Divi container that can directly accept inserted modules. + getDirectInsertElement( element ) { + const targetElement = this.getSelectableElement( element ) + if ( ! targetElement ) { + return null + } + + const targetModuleId = this.getModuleIdFromElement( targetElement ) + const targetModuleName = this.getModuleName( targetModuleId ) + if ( DIVI_DIRECT_INSERT_MODULE_NAMES.includes( targetModuleName ) ) { + return targetElement + } + + return targetElement.querySelector?.( + '.et_pb_column[data-id], .et_pb_column_inner[data-id], .et_pb_group[data-id], .et_pb_group_carousel[data-id]' + ) || null + } + + syncInteractionTargetElement( element, interactionTarget ) { + if ( ! element || ! interactionTarget ) { + return + } + + // Mirror the saved target onto the live builder DOM immediately so picker + // previews can work before Divi re-renders the module from store state. + element.setAttribute( 'data-interaction-target', interactionTarget ) + } + + buildTargetFromElement( element ) { + const targetElement = this.getSelectableElement( element ) + if ( ! targetElement ) { + return null + } + + // Resolve the clicked DOM node back to the Divi module record, then map it + // to the persistent interaction target we expose to the Interactions UI. + const moduleId = this.getModuleIdFromElement( targetElement ) + const interactionTarget = moduleId + ? this.ensureInteractionTarget( moduleId ) + : '' + if ( ! interactionTarget ) { + return null + } + this.syncInteractionTargetElement( targetElement, interactionTarget ) + + const moduleClass = Array.from( targetElement.classList ).find( className => + /^et_pb_(section|row|row_inner|column|column_inner|[a-z0-9_]+)$/i.test( className ) && + ! /^et_pb_[a-z0-9_]+_(?:\d+|[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12})$/i.test( className ) + ) || targetElement.tagName?.toLowerCase() || 'divi-element' + + return { + type: 'selector', + value: `[data-interaction-target="${ interactionTarget }"]`, + blockName: moduleClass, + } + } + + getCurrentSelectedTarget() { + return this.buildTargetFromElement( this.selectedElement?.element || null ) + } + + canInsertPreset( preset ) { + const example = getPresetBuilderExample( preset, 'divi' ) + return Array.isArray( example ) ? example.length > 0 : !! example + } + + // Return the builder module name for a Divi content node. + getModuleName( moduleId ) { + if ( ! moduleId ) { + return '' + } + + const moduleName = this.getDiviDataApi()?.select?.( 'divi/edit-post' )?.getModuleName?.( moduleId ) || '' + return typeof moduleName === 'string' ? moduleName : '' + } + + // Build a standard Interactions target from a persisted Divi module ID. + buildTargetFromModuleId( moduleId ) { + if ( ! moduleId ) { + return null + } + + const interactionTarget = this.ensureInteractionTarget( moduleId ) + if ( ! interactionTarget ) { + return null + } + + const targetElement = this.getCanvasDocument()?.querySelector( + `[data-id="${ moduleId }"], [data-wrapper-id="${ moduleId }"], [data-module-id="${ moduleId }"]` + ) + this.syncInteractionTargetElement( targetElement, interactionTarget ) + + return { + type: 'selector', + value: `[data-interaction-target="${ interactionTarget }"]`, + blockName: this.getModuleName( moduleId ) || 'divi-element', + } + } + + // Decide where a new Divi module should be inserted relative to the current selection. + getInsertTarget() { + const selectedInsertElement = this.getDirectInsertElement( this.selectedElement?.element || null ) + const selectedInsertModuleId = this.getModuleIdFromElement( selectedInsertElement ) + if ( selectedInsertModuleId ) { + return { + anchorId: selectedInsertModuleId, + position: 'inside', + } + } + + const selectedModuleId = this.getModuleIdFromElement( this.selectedElement?.element || null ) + if ( selectedModuleId ) { + return { + anchorId: selectedModuleId, + position: DIVI_CONTAINER_MODULE_NAMES.includes( this.getModuleName( selectedModuleId ) ) + ? 'inside' + : 'after', + } + } + + const previewDocument = this.getCanvasDocument() + const preferredContainer = previewDocument?.querySelector( + '.et_pb_column[data-id], .et_pb_column_inner[data-id], .et_pb_group[data-id], .et_pb_group_carousel[data-id]' + ) + const preferredContainerId = this.getModuleIdFromElement( preferredContainer ) + + if ( preferredContainerId ) { + return { + anchorId: preferredContainerId, + position: 'inside', + } + } + + return { + anchorId: 'root', + position: 'inside', + } + } + + // Normalize top-level Divi preset examples to an array for sequential insertion. + getDiviPresetNodes( preset ) { + const example = getPresetBuilderExample( preset, 'divi' ) + if ( Array.isArray( example ) ) { + return example + } + + return example ? [ example ] : [] + } + + // Resolve target refs by mapping the preset source ID to the inserted Divi module ID. + resolveInsertedTargetMapping( mapping = {}, inserted, diviTargetRefs = {} ) { + const sourceId = diviTargetRefs?.[ mapping.targetRef ]?.id + if ( ! sourceId ) { + return null + } + + const insertedId = inserted?.sourceIdToInsertedId?.[ sourceId ] + if ( ! insertedId ) { + return null + } + + return this.buildTargetFromModuleId( insertedId ) + } + + // Collect the source subtree IDs in the same pre-order Divi clone keeps them. + getPresetStructureIds( moduleObjects, moduleId ) { + if ( ! moduleId ) { + return [] + } + + const moduleChildren = moduleObjects?.getIn?.( [ moduleId, 'children' ], [] ) || [] + return [ + moduleId, + ...moduleChildren.flatMap( childId => + this.getPresetStructureIds( moduleObjects, childId ) + ), + ] + } + + // Read the module appearance limits Divi uses while cloning payload nodes. + getModuleAppearanceSettings( rootIds = [], moduleObjects ) { + const moduleLibrarySelect = this.getDiviDataApi()?.select?.( 'divi/module-library' ) + + return rootIds.reduce( ( appearanceSettings, rootId ) => { + for ( const moduleId of this.getPresetStructureIds( moduleObjects, rootId ) ) { + const moduleName = moduleObjects?.getIn?.( [ moduleId, 'name' ], '' ) + if ( ! moduleName || appearanceSettings[ moduleName ] ) { + continue + } + + const moduleDefinition = moduleLibrarySelect?.getModule?.( moduleName ) + if ( moduleDefinition?.appearance ) { + appearanceSettings[ moduleName ] = moduleDefinition.appearance + } + } + + return appearanceSettings + }, {} ) + } + + // Insert a preset into Divi by replaying its flat module payload through Divi's copy actions. + async insertPresetContent( preset ) { + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:start', { + presetId: preset?.id, + presetName: preset?.name, + } ) + if ( ! this.canInsertPreset( preset ) ) { + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:cannot-insert', { + presetId: preset?.id, + } ) + return null + } + + const presetNodes = this.getDiviPresetNodes( preset ) + if ( presetNodes.length === 0 ) { + return null + } + + const editPostSelect = this.getDiviDataApi()?.select?.( 'divi/edit-post' ) + const layout = editPostSelect?.getActiveLayout?.() || '' + const insertTarget = this.getInsertTarget() + if ( ! insertTarget?.anchorId ) { + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:no-anchor', { + presetId: preset?.id, + } ) + return null + } + + const editPostApi = this.getDiviDataApi() + const editPostDispatch = editPostApi?.dispatch?.( 'divi/edit-post' ) + const moduleSelect = editPostApi?.select?.( 'divi/module' ) + const payload = buildDiviPresetPayload( presetNodes ) + const targetParentId = insertTarget.position === 'inside' + ? insertTarget.anchorId + : editPostSelect?.getParentModuleId?.( insertTarget.anchorId ) + const insertSimpleNodeWithAddModule = async () => { + const simpleNode = presetNodes[ 0 ] + const contentBeforeAddModule = editPostSelect.getContent( layout || undefined ) + const childrenBeforeAddModule = toArray( + contentBeforeAddModule?.getIn?.( [ targetParentId, 'children' ], [] ) + ) + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:addModule', { + presetId: preset?.id, + insertTarget, + layout, + moduleName: simpleNode.name, + } ) + editPostDispatch.addModule( + insertTarget.anchorId, + simpleNode.name, + simpleNode.props || {}, + insertTarget.position, + '', + 'user', + layout || undefined + ) + + const { + insertedId, + parentChildren, + insertedChildren, + } = await waitForDiviInsertedModule( { + editPostSelect, + layout, + targetParentId, + insertTarget, + previousChildren: childrenBeforeAddModule, + } ) + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:addModule-result', { + presetId: preset?.id, + insertedId, + childrenBeforeAddModule, + parentChildren, + insertedChildren, + } ) + const defaultTarget = insertedId + ? this.buildTargetFromModuleId( insertedId ) + : null + + if ( ! defaultTarget ) { + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:addModule-no-target', { + presetId: preset?.id, + insertedId, + } ) + return null + } + + const inserted = { + rootIds: [ insertedId ], + sourceIdToInsertedId: simpleNode.id + ? { [ simpleNode.id ]: insertedId } + : {}, + } + const targetRefs = getPresetBuilderTargetRefs( preset, 'divi' ) + + return { + targetMappingsSource: inserted, + resolveTargetMappingTarget: mapping => this.resolveInsertedTargetMapping( + mapping, + inserted, + targetRefs + ) || defaultTarget, + defaultTarget, + targetRefs, + } + } + + if ( + payload.rootIds.length === 0 || + ! targetParentId || + ! editPostSelect?.getContent + ) { + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:missing-prerequisite', { + presetId: preset?.id, + rootIds: payload.rootIds, + targetParentId, + hasGetContent: !! editPostSelect?.getContent, + } ) + return null + } + + if ( + presetNodes.length === 1 && + ! presetNodes[ 0 ]?.children?.length && + editPostDispatch?.addModule && + ( + ! editPostDispatch?.copyModuleFromPayload || + ! editPostDispatch?.copyModulesFromPayload + ) + ) { + return insertSimpleNodeWithAddModule() + } + + if ( + ! editPostDispatch?.copyModuleFromPayload || + ! editPostDispatch?.copyModulesFromPayload + ) { + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:no-copy-api', { + presetId: preset?.id, + } ) + return null + } + + const contentBeforeInsert = editPostSelect.getContent( layout || undefined ) + const childrenBeforeInsert = toArray( + contentBeforeInsert?.getIn?.( [ targetParentId, 'children' ], [] ) + ) + const copyParams = { + payload, + position: insertTarget.position, + targetId: insertTarget.anchorId, + moduleAppearanceSettings: this.getModuleAppearanceSettings( + payload.rootIds, + payload.moduleObjects + ), + themeBuilderLayout: layout || undefined, + moduleCount: moduleSelect?.getModuleCount?.(), + caller: 'user', + } + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:copyModules', { + presetId: preset?.id, + insertTarget, + layout, + rootIds: payload.rootIds, + targetParentId, + } ) + + if ( payload.rootIds.length === 1 ) { + const firstRootId = payload.rootIds[ 0 ] + const firstModuleName = payload.moduleObjects.getIn( [ firstRootId, 'name' ], '' ) + editPostDispatch.copyModuleFromPayload( copyParams, firstModuleName ) + } else { + editPostDispatch.copyModulesFromPayload( copyParams ) + } + + const { + insertedId: firstInsertedId, + parentChildren: childrenAfterInsert, + insertedChildren: insertedRootIds, + } = await waitForDiviInsertedModule( { + editPostSelect, + layout, + targetParentId, + insertTarget, + previousChildren: childrenBeforeInsert, + } ) + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:copyModules-result', { + presetId: preset?.id, + childrenBeforeInsert, + childrenAfterInsert, + insertedRootIds, + firstInsertedId, + } ) + const defaultTarget = firstInsertedId + ? this.buildTargetFromModuleId( firstInsertedId ) + : null + if ( ! defaultTarget ) { + if ( + presetNodes.length === 1 && + ! presetNodes[ 0 ]?.children?.length && + editPostDispatch?.addModule + ) { + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:copyModules-fallback-addModule', { + presetId: preset?.id, + } ) + return insertSimpleNodeWithAddModule() + } + + // eslint-disable-next-line no-console + console.warn( 'Divi insertPresetContent:copyModules-no-target', { + presetId: preset?.id, + firstInsertedId, + insertedRootIds, + } ) + return null + } + + const inserted = { + rootIds: insertedRootIds, + sourceIdToInsertedId: {}, + } + + payload.rootIds.forEach( ( sourceRootId, rootIndex ) => { + const insertedRootId = insertedRootIds[ rootIndex ] + if ( ! insertedRootId ) { + return + } + + const sourceStructureIds = this.getPresetStructureIds( + payload.moduleObjects, + sourceRootId + ) + const insertedStructureIds = editPostSelect.getModuleStructureIds?.( insertedRootId ) || [] + + sourceStructureIds.forEach( ( sourceId, structureIndex ) => { + const insertedId = insertedStructureIds[ structureIndex ] + if ( insertedId ) { + inserted.sourceIdToInsertedId[ sourceId ] = insertedId + } + } ) + } ) + + const targetRefs = getPresetBuilderTargetRefs( preset, 'divi' ) + return { + targetMappingsSource: inserted, + resolveTargetMappingTarget: mapping => this.resolveInsertedTargetMapping( + mapping, + inserted, + targetRefs + ) || defaultTarget, + defaultTarget, + targetRefs, + } + } + + registerSelectionTracking() { + if ( this.selectionTrackingCleanup ) { + return this.selectionTrackingCleanup + } + + let boundDocument = null + let boundIframe = null + let observer = null + + const clickHandler = event => { + const candidate = this.getSelectableElement( event.target ) + if ( candidate ) { + this.selectedElement = { element: candidate } + } + } + + const unbindDocument = () => { + if ( boundDocument ) { + boundDocument.removeEventListener( 'click', clickHandler, true ) + boundDocument = null + } + } + + const bindDocument = previewDocument => { + if ( ! previewDocument?.body || boundDocument === previewDocument ) { + return + } + + unbindDocument() + previewDocument.addEventListener( 'click', clickHandler, true ) + boundDocument = previewDocument + } + + const syncBindings = () => { + const nextIframe = document.querySelector( 'iframe[src*="app_window=1"]' ) + if ( boundIframe && boundIframe !== nextIframe ) { + boundIframe.removeEventListener( 'load', syncBindings ) + boundIframe = null + unbindDocument() + } + + if ( nextIframe && boundIframe !== nextIframe ) { + // Re-run the binding step after every iframe reload so selection + // tracking follows Divi's canvas document as it gets replaced. + nextIframe.addEventListener( 'load', syncBindings ) + boundIframe = nextIframe + } + + bindDocument( this.getCanvasDocument() ) + } + + syncBindings() + + // Keep watching for iframe replacement because Divi can recreate the app + // window during builder navigation without reloading the top document. + observer = new MutationObserver( syncBindings ) + observer.observe( document.body, { + childList: true, + subtree: true, + } ) + + this.selectionTrackingCleanup = () => { + observer?.disconnect() + if ( boundIframe ) { + boundIframe.removeEventListener( 'load', syncBindings ) + } + unbindDocument() + boundIframe = null + this.selectionTrackingCleanup = null + } + + return this.selectionTrackingCleanup + } + + startElementPicker( { + onPick = NOOP, + onCancel = NOOP, + } = {} ) { + const previewDocument = this.getCanvasDocument() + if ( ! previewDocument ) { + onCancel() + return NOOP + } + + let highlightedElement = null + + const clearHighlight = () => { + if ( highlightedElement ) { + highlightedElement.style.outline = highlightedElement.dataset.interactPrevOutline || '' + highlightedElement.style.outlineOffset = highlightedElement.dataset.interactPrevOutlineOffset || '' + delete highlightedElement.dataset.interactPrevOutline + delete highlightedElement.dataset.interactPrevOutlineOffset + } + highlightedElement = null + } + + const mouseMoveHandler = event => { + const candidate = this.getSelectableElement( event.target ) + if ( candidate === highlightedElement ) { + return + } + + clearHighlight() + if ( candidate ) { + highlightedElement = candidate + highlightedElement.dataset.interactPrevOutline = highlightedElement.style.outline || '' + highlightedElement.dataset.interactPrevOutlineOffset = highlightedElement.style.outlineOffset || '' + highlightedElement.style.outline = '2px solid #05f' + highlightedElement.style.outlineOffset = '2px' + } + } + + // Capture the target on mousedown so Divi's own click-to-edit behavior + // does not consume the first interaction before we can resolve it. + const mouseDownHandler = event => { + const candidate = this.getSelectableElement( event.target ) + if ( ! candidate ) { + return + } + + event.preventDefault() + event.stopPropagation() + const target = this.buildTargetFromElement( candidate ) + stop() + + if ( target ) { + onPick( target ) + } else { + onCancel() + } + } + + const keyHandler = event => { + if ( event.key === 'Escape' ) { + stop() + onCancel() + } + } + + const stop = () => { + clearHighlight() + previewDocument.removeEventListener( 'mousemove', mouseMoveHandler, true ) + previewDocument.removeEventListener( 'mousedown', mouseDownHandler, true ) + previewDocument.removeEventListener( 'keydown', keyHandler, true ) + document.removeEventListener( 'keydown', keyHandler, true ) + } + + previewDocument.addEventListener( 'mousemove', mouseMoveHandler, true ) + previewDocument.addEventListener( 'mousedown', mouseDownHandler, true ) + previewDocument.addEventListener( 'keydown', keyHandler, true ) + document.addEventListener( 'keydown', keyHandler, true ) + + return stop + } +} + +export default DiviInteractionsEditor diff --git a/src/editor/editors/elementor.js b/src/editor/editors/elementor.js index 737b385..5e8dbee 100644 --- a/src/editor/editors/elementor.js +++ b/src/editor/editors/elementor.js @@ -1,6 +1,12 @@ import IconSVG from '../assets/icon.svg' import InteractionsApp from '../app' import InteractionsEditorAbstract from './abstract' +import { InteractionLibraryRoot } from '../interaction-library' +import { normalizeElementorExample } from '../interaction-library/elementor-example' +import { + getPresetBuilderExample, + getPresetBuilderTargetRefs, +} from '../interaction-library/preset-schema' import { __ } from '@wordpress/i18n' import { Button } from '@wordpress/components' @@ -60,7 +66,6 @@ class ElementorInteractionsEditor extends InteractionsEditorAbstract { return ( <> - - { /* { isOpen && ( -
setIsOpen( false ) } - aria-hidden="true" - /> - ) } */ }
@@ -102,6 +100,7 @@ class ElementorInteractionsEditor extends InteractionsEditorAbstract {
+ ) } @@ -130,6 +129,8 @@ class ElementorInteractionsEditor extends InteractionsEditorAbstract { return null } + // Some Elementor widgets render through editor-only wrappers, so the + // interaction target needs to point at the frontend child instead. getWidgetContentTargetSelector( targetElement, elementId ) { if ( ! targetElement || ! elementId ) { return '' @@ -215,6 +216,223 @@ class ElementorInteractionsEditor extends InteractionsEditorAbstract { return this.buildTargetFromElement( this.selectedElement?.element || null ) } + canInsertPreset( preset ) { + const example = getPresetBuilderExample( preset, 'elementor' ) + return Array.isArray( example ) && example.length > 0 + } + + // Insert into the nearest selected container. If the current selection is a + // widget, promote the insert target to its parent container so Elementor can + // paste sibling widgets alongside it. + getInsertContainer() { + let container = + this.selectedElement?.view?.getContainer?.() || + window.elementor?.getCurrentElement?.()?.getContainer?.() || + window.elementor?.getPreviewContainer?.() || + window.elementor?.getPreviewView?.()?.getContainer?.() || + null + + container = container?.lookup?.() || container + if ( ! container ) { + return null + } + + const elementType = container.model?.get?.( 'elType' ) || '' + if ( elementType === 'widget' ) { + return container.parent || null + } + + return container + } + + getFirstInsertedContainer( inserted ) { + if ( Array.isArray( inserted ) ) { + for ( const item of inserted ) { + const firstInsertedContainer = this.getFirstInsertedContainer( item ) + if ( firstInsertedContainer ) { + return firstInsertedContainer + } + } + return null + } + + return inserted?.lookup?.() || inserted || null + } + + // Read the minimum metadata we need from either the live preview DOM or the + // inserted container model. Insert mode can resolve before the preview node + // exists, so we cannot depend on the DOM alone here. + getContainerMeta( container ) { + const resolvedContainer = container?.lookup?.() || container + const model = resolvedContainer?.model || resolvedContainer + const element = resolvedContainer?.view?.$el?.get?.( 0 ) || null + const targetElement = element?.closest?.( '.elementor-element[data-id]' ) || null + + return { + element, + targetElement, + elementId: + targetElement?.getAttribute?.( 'data-id' ) || + model?.get?.( 'id' ) || + model?.id || + '', + elementType: + targetElement?.getAttribute?.( 'data-element_type' ) || + model?.get?.( 'elType' ) || + '', + widgetType: + targetElement?.getAttribute?.( 'data-widget_type' ) || + model?.get?.( 'widgetType' ) || + '', + } + } + + // Build a normal interaction target from an inserted Elementor container. + // This powers both the default target for simple presets and path-based + // targetRef resolution for complex presets. + buildTargetFromContainer( container, targetType = 'selector' ) { + const { + element, + elementId, + elementType, + widgetType, + } = this.getContainerMeta( container ) + + if ( element ) { + return this.buildTargetFromElement( element, targetType ) + } + + if ( ! elementId ) { + return null + } + + const wrapperSelector = `.elementor-element.elementor-element-${ elementId }` + const targetTargetType = targetType === 'class' ? 'class' : 'selector' + const targetValue = targetTargetType === 'class' + ? `elementor-element-${ elementId }` + : widgetType.startsWith( 'button.' ) + ? `${ wrapperSelector } a.elementor-button` + : widgetType.startsWith( 'icon.' ) + ? `${ wrapperSelector } .elementor-icon` + : wrapperSelector + + return { + type: targetTargetType, + value: targetValue, + blockName: widgetType || elementType || 'elementor-element', + } + } + + getWrapperSelectorFromContainer( container ) { + const { elementId } = this.getContainerMeta( container ) + + return elementId ? `.elementor-element.elementor-element-${ elementId }` : '' + } + + // Scope a preset's child selector to the inserted widget wrapper so + // `targetRefs` can point at nested frontend elements such as buttons. + buildChildSelectorTarget( container, targetConfig = {} ) { + const baseTarget = this.buildTargetFromContainer( container ) + if ( ! baseTarget ) { + return null + } + + const childSelector = targetConfig.value || '' + const wrapperSelector = this.getWrapperSelectorFromContainer( container ) || baseTarget.value + const scopedSelector = childSelector + ? `${ wrapperSelector } ${ childSelector }` + : wrapperSelector + + return { + type: targetConfig.type || 'selector', + value: scopedSelector, + blockName: targetConfig.blockName || baseTarget.blockName, + options: targetConfig.options || '', + } + } + + // Resolve a semantic targetRef from the preset into a runtime interaction + // target by following the inserted tree path returned by Elementor. + resolveInsertedTargetMapping( mapping = {}, inserted, elementorTargetRefs = {} ) { + const targetRefConfig = elementorTargetRefs?.[ mapping.targetRef ] + const targetPath = Array.isArray( targetRefConfig ) + ? targetRefConfig + : targetRefConfig?.path + + if ( ! Array.isArray( targetPath ) ) { + return null + } + + const container = targetPath.reduce( ( currentValue, key ) => currentValue?.[ key ], inserted ) + if ( ! container ) { + return null + } + + if ( targetRefConfig?.target ) { + return this.buildChildSelectorTarget( container, targetRefConfig.target ) + } + + return this.buildTargetFromContainer( container ) + } + + // Insert a preset into Elementor through its paste command so the builder + // can create any required wrapper containers automatically. The returned + // context is later consumed by the shared library flow to resolve either: + // 1. explicit targetRefs/targetMappings, or + // 2. a default target for simple presets with only `elementorExample`. + async insertPresetContent( preset ) { + if ( ! this.canInsertPreset( preset ) || ! window.$e?.run ) { + return null + } + + const container = this.getInsertContainer() + if ( ! container ) { + return null + } + + const normalizedExample = normalizeElementorExample( getPresetBuilderExample( preset, 'elementor' ) ) + if ( normalizedExample.length === 0 ) { + return null + } + + const inserted = await window.$e.run( 'document/elements/paste', { + container, + rebuild: true, + storageType: 'json', + data: JSON.stringify( { + type: 'elementor', + elements: normalizedExample, + } ), + options: { + at: container.view?.collection?.length, + }, + } ) + + // The first inserted container becomes the fallback target for presets + // that do not declare target mappings. + const firstInsertedContainer = this.getFirstInsertedContainer( inserted ) + if ( ! firstInsertedContainer ) { + return null + } + const defaultTarget = this.buildTargetFromContainer( firstInsertedContainer ) + + window.$e.internal?.( 'document/save/set-is-modified', { status: true } ) + const targetRefs = getPresetBuilderTargetRefs( preset, 'elementor' ) + + return { + targetMappingsSource: inserted, + // Prefer an explicit mapped target, but fall back to the first inserted + // element so simple single-widget presets still come in fully targeted. + resolveTargetMappingTarget: mapping => this.resolveInsertedTargetMapping( + mapping, + inserted, + targetRefs + ) || defaultTarget, + defaultTarget, + targetRefs, + } + } + // Track the current Elementor selection from the editor panel. registerSelectionTracking() { if ( ! window.elementor?.hooks?.addAction ) { diff --git a/src/editor/editors/gutenberg.js b/src/editor/editors/gutenberg.js index 3de9f3a..b533a9c 100644 --- a/src/editor/editors/gutenberg.js +++ b/src/editor/editors/gutenberg.js @@ -1,8 +1,10 @@ import IconSVG from '../assets/icon.svg' import InteractionsApp from '../app' import InteractionsEditorAbstract from './abstract' -import { InteractionLibrary } from '../interaction-library' +import { InteractionLibraryRoot } from '../interaction-library' +import { getPresetBuilderExample } from '../interaction-library/preset-schema' +import { parse } from '@wordpress/blocks' import { registerPlugin } from '@wordpress/plugins' import { __ } from '@wordpress/i18n' import { @@ -53,19 +55,11 @@ class GutenbergInteractionsEditor extends InteractionsEditorAbstract { ) } - const GutenbergInteractionLibraryComponent = () => { - const interactionLibraryMode = useSelect( select => - select( 'interact/interaction-library-modal' ).getMode(), - [] ) - - return interactionLibraryMode ? : null - } - registerPlugin( 'interact-editor', { render: GutenbergInteractionsEditorComponent, } ) registerPlugin( 'interact-editor-library', { - render: GutenbergInteractionLibraryComponent, + render: InteractionLibraryRoot, } ) return super.init() @@ -126,6 +120,30 @@ class GutenbergInteractionsEditor extends InteractionsEditorAbstract { options: '', } } + + // Persist the current post when the library or interaction editor saves. + saveEditor() { + return Promise.resolve( dispatch( 'core/editor' )?.savePost?.() ) + } + + canInsertPreset( preset ) { + return !! getPresetBuilderExample( preset, 'gutenberg' ) + } + + // Insert the preset block tree and return it so target mappings can resolve + // against the freshly inserted Gutenberg blocks. + insertPresetContent( preset ) { + const [ block ] = parse( getPresetBuilderExample( preset, 'gutenberg' ) ?? '' ) + if ( ! block ) { + return null + } + + dispatch( 'core/block-editor' ).insertBlocks( block ) + + return { + targetMappingsSource: block, + } + } } export default GutenbergInteractionsEditor diff --git a/src/editor/editors/index.js b/src/editor/editors/index.js index fba10b5..80e7ad6 100644 --- a/src/editor/editors/index.js +++ b/src/editor/editors/index.js @@ -2,6 +2,7 @@ import { editorMode } from 'interactions' import GutenbergInteractionsEditor from './gutenberg' import ElementorInteractionsEditor from './elementor' import BricksInteractionsEditor from './bricks' +import DiviInteractionsEditor from './divi' let activeEditor = null @@ -11,7 +12,9 @@ const createInteractionsEditor = () => { ? new ElementorInteractionsEditor() : editorMode === 'bricks' ? new BricksInteractionsEditor() - : new GutenbergInteractionsEditor() + : editorMode === 'divi' + ? new DiviInteractionsEditor() + : new GutenbergInteractionsEditor() } // Return the memoized editor adapter instance. @@ -28,6 +31,8 @@ export const isElementorEditor = () => getInteractionsEditor().isElementor() export const isBricksEditor = () => getInteractionsEditor().isBricks() +export const isDiviEditor = () => getInteractionsEditor().isDivi() + export const isGutenbergEditor = () => getInteractionsEditor().isGutenberg() export const isBuilderEditor = () => getInteractionsEditor().isBuilder() @@ -47,3 +52,10 @@ export const getCurrentSelectedTarget = () => getInteractionsEditor().getCurrent export const registerEditorSelectionTracking = () => getInteractionsEditor().registerSelectionTracking() export const startEditorElementPicker = args => getInteractionsEditor().startElementPicker( args ) + +export const saveCurrentEditor = () => getInteractionsEditor().saveEditor() + +export const insertLibraryPreset = selectedPreset => getInteractionsEditor().insertLibraryPreset( selectedPreset ) + +export const resolveLibraryPresetTargets = ( interactionSetup, selectedPreset, insertionContext ) => + getInteractionsEditor().resolveLibraryPresetTargets( interactionSetup, selectedPreset, insertionContext ) diff --git a/src/editor/interaction-library/bricks-example.js b/src/editor/interaction-library/bricks-example.js new file mode 100644 index 0000000..5da95a9 --- /dev/null +++ b/src/editor/interaction-library/bricks-example.js @@ -0,0 +1,107 @@ +import { customAlphabet } from 'nanoid' + +const generateBricksElementId = customAlphabet( '1234567890abcdefghijklmnopqrstuvwxyz', 7 ) + +/** + * Normalize a Bricks preset example into the flat element array shape Bricks + * stores in post meta. + * + * Examples may already be flat arrays, but this also supports nested + * `children` objects so presets can stay readable when needed. + * + * @param {Array|Object} example Raw Bricks example payload. + * + * @return {Array} Flat Bricks elements array. + */ +export const normalizeBricksExample = example => { + const sourceElements = Array.isArray( example ) ? example : [ example ] + const hasNestedChildren = sourceElements.some( element => + Array.isArray( element?.children ) && + element.children.some( child => child && typeof child === 'object' && ! Array.isArray( child ) ) + ) + + if ( ! hasNestedChildren ) { + return sourceElements + .filter( Boolean ) + .map( element => ( { + ...structuredClone( element ), + id: element.id || generateBricksElementId(), + children: Array.isArray( element.children ) ? [ ...element.children ] : [], + } ) ) + } + + const flattenedElements = [] + + const visit = ( element, parentId = '' ) => { + if ( ! element || typeof element !== 'object' ) { + return null + } + + const elementId = element.id || generateBricksElementId() + const rawChildren = Array.isArray( element.children ) ? element.children : [] + const childObjects = rawChildren.filter( child => child && typeof child === 'object' && ! Array.isArray( child ) ) + const childIds = rawChildren.filter( child => typeof child === 'string' ) + const normalizedElement = structuredClone( element ) + + normalizedElement.id = elementId + normalizedElement.children = [ ...childIds ] + + if ( parentId ) { + normalizedElement.parent = parentId + } else { + delete normalizedElement.parent + } + + flattenedElements.push( normalizedElement ) + + childObjects.forEach( child => { + const childElementId = visit( child, elementId ) + if ( childElementId ) { + normalizedElement.children.push( childElementId ) + } + } ) + + return elementId + } + + sourceElements.forEach( element => visit( element ) ) + + return flattenedElements +} + +/** + * Clone a Bricks preset example for insertion and regenerate every element ID. + * + * Returning the source-to-inserted ID map lets target refs keep referring to + * stable preset IDs while the saved Bricks content uses fresh IDs each time. + * + * @param {Array|Object} example Raw Bricks example payload. + * + * @return {Object} Insert-ready elements and ID lookup data. + */ +export const cloneBricksExample = example => { + const normalizedExample = normalizeBricksExample( example ) + const sourceIdToInsertedId = normalizedExample.reduce( ( accumulator, element ) => { + accumulator[ element.id ] = generateBricksElementId() + return accumulator + }, {} ) + + const elements = normalizedExample.map( element => ( { + ...structuredClone( element ), + id: sourceIdToInsertedId[ element.id ], + parent: element.parent ? sourceIdToInsertedId[ element.parent ] : '', + children: Array.isArray( element.children ) + ? element.children.map( childId => sourceIdToInsertedId[ childId ] || childId ) + : [], + } ) ) + + const rootElementIds = elements + .filter( element => ! element.parent ) + .map( element => element.id ) + + return { + elements, + rootElementIds, + sourceIdToInsertedId, + } +} diff --git a/src/editor/interaction-library/configure-modal.js b/src/editor/interaction-library/configure-modal.js index e201bcc..a4d8059 100644 --- a/src/editor/interaction-library/configure-modal.js +++ b/src/editor/interaction-library/configure-modal.js @@ -1,9 +1,7 @@ /** * Internal deprendencies */ -import { - setValueAtPath, addLoopDelayToPreview, applyTargetMappings, -} from './util' +import { setValueAtPath, addLoopDelayToPreview } from './util' import { PropertyControl } from '../components/timeline/property-control' import TargetSelector from '../components/target-selector' import getVideoUrl from './videos' @@ -12,6 +10,11 @@ import { useInteractions } from '../hooks' import { openInteractionsSidebar, createNewAction, createNewInteraction, } from '~interact/editor/util' +import { + insertLibraryPreset, + isBuilderEditor, + resolveLibraryPresetTargets, +} from '~interact/editor/editors' /** * External deprendencies @@ -20,11 +23,13 @@ import { /** * WordPress deprendencies */ -import { Button } from '@wordpress/components' -import { parse } from '@wordpress/blocks' +import { + Button, + Spinner, +} from '@wordpress/components' import { dispatch } from '@wordpress/data' import { - useState, useMemo, useEffect, + useState, useMemo, useEffect, useRef, useCallback, } from '@wordpress/element' import { __ } from '@wordpress/i18n' @@ -43,12 +48,27 @@ export const ConfigureModal = props => { const [ optionValues, setOptionValues ] = useState( {} ) const [ selectedTarget, setSelectedTarget ] = useState( interactionTarget ) const [ sideBarEl, setSideBarEl ] = useState( null ) + const [ isApplying, setIsApplying ] = useState( !! selectedPreset.skipConfig ) + const didAutoApplyRef = useRef( false ) + const selectedTargetLabel = isBuilderEditor() + ? __( 'This interaction will be applied to the selected element. Click here to modify.', 'interactions' ) + : __( 'This interaction will be applied to the selected block. Click here to modify.', 'interactions' ) useEffect( () => { // Set the editor sidebar as anchor for target selector - setSideBarEl( document.querySelector( '.interface-interface-skeleton__sidebar' ) || null ) + setSideBarEl( + document.querySelector( '.interface-interface-skeleton__sidebar, .interact-pagebuilder-sidebar' ) || null + ) }, [] ) + useEffect( () => { + didAutoApplyRef.current = false + }, [ selectedPreset ] ) + + useEffect( () => { + setIsApplying( !! selectedPreset.skipConfig ) + }, [ selectedPreset ] ) + const interactionSetup = useMemo( () => ( selectedPreset.interactionSetup ), [ selectedPreset ] ) const configurableOptions = useMemo( () => selectedPreset.configurableOptions ?? [], [ selectedPreset ] ) @@ -56,7 +76,9 @@ export const ConfigureModal = props => { updateInteraction, } = useInteractions() - const handleApply = () => { + const handleApply = useCallback( async () => { + setIsApplying( true ) + const config = getConfig( selectedPreset.config ) let isRunDefaultConfig = true // Allow an entry to handle configurations and block generation for complex interactions @@ -80,21 +102,18 @@ export const ConfigureModal = props => { } ) } ) - const targetMappings = selectedPreset.targetMappings - - // If mode is inset, create a new block based on the seralized example. - // Otherwise, use the target from target selector. + // In insert mode, let the active editor adapter own how preset content + // is created so this modal no longer depends on Gutenberg block APIs. if ( mode === 'insert' ) { - const block = parse( selectedPreset.serializedBlockExample ?? '' )[ 0 ] - if ( ! block ) { + const insertedContent = await insertLibraryPreset( selectedPreset ) + if ( ! insertedContent ) { + setIsApplying( false ) return } - dispatch( 'core/block-editor' ).insertBlocks( block ) - // If target mappings are provided, dynamically create target for each. - applyTargetMappings( interactionSetup, targetMappings, block, ) + resolveLibraryPresetTargets( interactionSetup, selectedPreset, insertedContent ) } else if ( mode === 'apply' ) { - applyTargetMappings( interactionSetup, targetMappings, selectedTarget ) + resolveLibraryPresetTargets( interactionSetup, selectedPreset, selectedTarget ) } } @@ -139,7 +158,6 @@ export const ConfigureModal = props => { }, } ) ) window?.dispatchEvent( new CustomEvent( 'interact/save-interaction' ) ) - dispatch( 'core/editor' ).savePost() }, 100 ) } else { const newInteraction = createNewInteraction( @@ -150,12 +168,54 @@ export const ConfigureModal = props => { updateInteraction( newInteraction ) } } ) - } - // If skipConfig is true, just apply the interaction user without configuration. - if ( selectedPreset.skipConfig ) { + setIsApplying( false ) + }, [ + closeModal, + configurableOptions, + interactionSetup, + mode, + optionValues, + selectedPreset, + selectedTarget, + updateInteraction, + ] ) + + useEffect( () => { + // Auto-apply skip-config presets once after the modal mounts for the + // selected preset, instead of triggering inserts during render. + if ( ! selectedPreset.skipConfig || didAutoApplyRef.current ) { + return + } + + didAutoApplyRef.current = true handleApply() - return null + }, [ selectedPreset, handleApply ] ) + + // Skip-config presets apply immediately and do not render controls. + if ( selectedPreset.skipConfig ) { + return ( +
+ +

+ { mode === 'insert' + ? __( 'Inserting interaction…', 'interactions' ) + : __( 'Applying interaction…', 'interactions' ) + } +

+
+ ) } return ( @@ -209,7 +269,7 @@ export const ConfigureModal = props => { { mode === 'apply' && (
- { __( 'This interaction will be applied to the selected block. Click here to modify.', 'interactions' ) } + { selectedTargetLabel } { diff --git a/src/editor/interaction-library/elementor-example.js b/src/editor/interaction-library/elementor-example.js new file mode 100644 index 0000000..8ec369a --- /dev/null +++ b/src/editor/interaction-library/elementor-example.js @@ -0,0 +1,98 @@ +const ELEMENTOR_META_KEYS = [ 'unit', 'sizes', 'isLinked' ] + +/** + * Remove empty values from copied Elementor settings while preserving + * meaningful falsy values such as 0 and false. + * + * @param {*} value Raw copied Elementor value. + * + * @return {*} Pruned value, or undefined when empty. + */ +const pruneElementorValue = value => { + if ( value === null || value === undefined || value === '' ) { + return undefined + } + + if ( Array.isArray( value ) ) { + const prunedArray = value + .map( item => pruneElementorValue( item ) ) + .filter( item => item !== undefined ) + + return prunedArray.length > 0 ? prunedArray : undefined + } + + if ( typeof value !== 'object' ) { + return value + } + + const prunedObject = Object.entries( value ).reduce( ( accumulator, [ key, nestedValue ] ) => { + const prunedValue = pruneElementorValue( nestedValue ) + if ( prunedValue !== undefined ) { + accumulator[ key ] = prunedValue + } + return accumulator + }, {} ) + + const keys = Object.keys( prunedObject ) + if ( keys.length === 0 ) { + return undefined + } + + // Drop Elementor control placeholders that only keep metadata such as + // `unit`, `sizes`, or `isLinked` but no real configured value. + const hasMeaningfulValue = keys.some( key => ! ELEMENTOR_META_KEYS.includes( key ) ) + if ( ! hasMeaningfulValue ) { + return undefined + } + + return prunedObject +} + +/** + * Normalize a single copied Elementor element into the minimal structure + * needed by the paste command. + * + * @param {Object} element Raw Elementor element data. + * + * @return {?Object} Normalized Elementor element. + */ +const normalizeElementorNode = element => { + if ( ! element || typeof element !== 'object' ) { + return null + } + + const normalizedChildren = Array.isArray( element.elements ) + ? element.elements + .map( child => normalizeElementorNode( child ) ) + .filter( Boolean ) + : [] + + return pruneElementorValue( { + id: element.id, + elType: element.elType, + widgetType: element.widgetType, + isInner: element.isInner, + settings: pruneElementorValue( + element.settings && typeof element.settings === 'object' + ? { ...element.settings } + : {} + ) || {}, + elements: normalizedChildren, + } ) +} + +/** + * Normalize copied Elementor editor JSON so presets can store raw copied + * structures while the paste command receives a predictable array shape. + * + * @param {Array|Object} example Raw copied Elementor example data. + * + * @return {Array} Normalized Elementor example tree. + */ +export const normalizeElementorExample = example => { + const elements = Array.isArray( example ) ? example : [ example ] + + return elements + .map( element => normalizeElementorNode( element ) ) + .filter( Boolean ) +} diff --git a/src/editor/interaction-library/index.js b/src/editor/interaction-library/index.js index 1c153a7..6c72c45 100644 --- a/src/editor/interaction-library/index.js +++ b/src/editor/interaction-library/index.js @@ -5,6 +5,10 @@ import './store' import { SelectModal } from './select-modal' import { ConfigureModal } from './configure-modal' import { isPresetApplicable, useInteractionPresets } from './util' +import { + getInteractionsEditor, + isGutenbergEditor, +} from '~interact/editor/editors' /** * External deprendencies @@ -120,6 +124,8 @@ const APPLY_CATEGORIES = [ ] export const InteractionLibrary = () => { + const isGutenberg = isGutenbergEditor() + const interactionsEditor = getInteractionsEditor() const { interactionTarget, interactionMode, favorites, } = useSelect( select => { @@ -154,11 +160,14 @@ export const InteractionLibrary = () => { // Sort all first based on applicability and categories. const sortedPresets = useMemo( () => { + const availablePresets = interactionMode === 'insert' + ? interactionPresets.filter( preset => interactionsEditor.canInsertPreset( preset ) ) + : interactionPresets let applicable = [] const notApplicable = [] - if ( interactionTarget?.blockName ) { - interactionPresets.forEach( preset => { + if ( interactionMode === 'apply' && isGutenberg && interactionTarget?.blockName ) { + availablePresets.forEach( preset => { if ( isPresetApplicable( preset, interactionTarget.blockName ) ) { applicable.push( { ...preset, isApplicable: true } ) } else { @@ -166,7 +175,10 @@ export const InteractionLibrary = () => { } } ) } else { - applicable = interactionPresets + applicable = availablePresets.map( preset => ( { + ...preset, + isApplicable: true, + } ) ) } // Make a sorter function @@ -189,7 +201,7 @@ export const InteractionLibrary = () => { notApplicable.sort( sorter ) return [ ...applicable, ...notApplicable ] - }, [ interactionTarget, categoryPriority, interactionPresets ] ) + }, [ interactionTarget, categoryPriority, interactionPresets, interactionsEditor, isGutenberg, interactionMode ] ) const handleClose = () => { // Close the modal and reset the interaction library target. @@ -273,3 +285,11 @@ export const InteractionLibrary = () => { ) } + +export const InteractionLibraryRoot = () => { + const interactionLibraryMode = useSelect( select => + select( 'interact/interaction-library-modal' ).getMode(), + [] ) + + return interactionLibraryMode ? : null +} diff --git a/src/editor/interaction-library/library/background-color-transition.json b/src/editor/interaction-library/library/background-color-transition.json index b98b221..f8dc4d4 100644 --- a/src/editor/interaction-library/library/background-color-transition.json +++ b/src/editor/interaction-library/library/background-color-transition.json @@ -228,5 +228,111 @@ ] } ], - "serializedBlockExample": "\r\n
\r\n
\r\n
\"\"
\r\n\r\n\r\n\r\n

Verdant

\r\n\r\n\r\n\r\n

Lush greenery symbolizing life and growth. A bold statement of freshness and vitality. It speaks to renewal, thriving energy, and natural abundance. Perfect for brands that celebrate sustainability, wellness, or pure living.

\r\n
\r\n
\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n
\r\n
\r\n
\"\"
\r\n\r\n\r\n\r\n

Verdant

\r\n\r\n\r\n\r\n

Lush greenery symbolizing life and growth. A bold statement of freshness and vitality. It speaks to renewal, thriving energy, and natural abundance. Perfect for brands that celebrate sustainability, wellness, or pure living.

\r\n
\r\n
\r\n", + "elementorExample": [ + { + "id": "interact-background-color-transition", + "elType": "container", + "settings": { + "content_width": "boxed", + "padding": { + "unit": "px", + "top": "48", + "right": "48", + "bottom": "48", + "left": "48", + "isLinked": false + }, + "background_background": "classic", + "background_color": "#ffffff", + "border_radius": { + "unit": "px", + "top": "15", + "right": "15", + "bottom": "15", + "left": "15", + "isLinked": true + } + }, + "elements": [ + { + "id": "interact-background-color-transition-heading", + "elType": "widget", + "widgetType": "heading", + "settings": { + "title": "Verdant" + } + }, + { + "id": "interact-background-color-transition-text", + "elType": "widget", + "widgetType": "text-editor", + "settings": { + "editor": "

Lush greenery symbolizing life and growth. A bold statement of freshness and vitality. It speaks to renewal, thriving energy, and natural abundance.

" + } + } + ] + } + ], + "bricksExample": [ + { + "id": "interact-background-color-transition", + "name": "container", + "settings": { + "background": { + "color": { + "hex": "#ffffff" + } + } + }, + "children": [ + { + "id": "interact-background-color-transition-heading", + "name": "heading", + "settings": { + "text": "Verdant" + } + }, + { + "id": "interact-background-color-transition-text", + "name": "text-basic", + "settings": { + "text": "

Lush greenery symbolizing life and growth. A bold statement of freshness and vitality. It speaks to renewal, thriving energy, and natural abundance.

" + } + } + ] + } + ], + "diviExample": { + "id": "interact-background-color-transition", + "name": "divi/group", + "children": [ + { + "id": "interact-background-color-transition-heading", + "name": "divi/text", + "props": { + "content": { + "innerContent": { + "desktop": { + "value": "

Verdant

" + } + } + } + } + }, + { + "id": "interact-background-color-transition-text", + "name": "divi/text", + "props": { + "content": { + "innerContent": { + "desktop": { + "value": "

Lush greenery symbolizing life and growth. A bold statement of freshness and vitality. It speaks to renewal, thriving energy, and natural abundance.

" + } + } + } + } + } + ] + } +} diff --git a/src/editor/interaction-library/library/bento-entrance-video.json b/src/editor/interaction-library/library/bento-entrance-video.json index 6a8c927..825fa41 100644 --- a/src/editor/interaction-library/library/bento-entrance-video.json +++ b/src/editor/interaction-library/library/bento-entrance-video.json @@ -91,6 +91,6 @@ ], "options": [] }, - "serializedBlockExample": "\r\n
\r\n
\r\n\r\n\r\n\r\n

Crafting Wood, Creating Legacy

\r\n\r\n\r\n\r\n
\r\n
\r\n

Quality woodwork built with passion, precision, and timeless craftsmanship.

\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n
\r\n", - "applyWhitelist": [] -} \ No newline at end of file + "applyWhitelist": [], + "gutenbergExample": "\r\n
\r\n
\r\n\r\n\r\n\r\n

Crafting Wood, Creating Legacy

\r\n\r\n\r\n\r\n
\r\n
\r\n

Quality woodwork built with passion, precision, and timeless craftsmanship.

\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n
\r\n" +} diff --git a/src/editor/interaction-library/library/bouncing-button.json b/src/editor/interaction-library/library/bouncing-button.json index 56d0974..b54b912 100644 --- a/src/editor/interaction-library/library/bouncing-button.json +++ b/src/editor/interaction-library/library/bouncing-button.json @@ -164,16 +164,73 @@ ] } ], - "serializedBlockExample": "\r\n
\r\n\r\n
\r\n", "targetMappings": [ { - "blockPath": [ - "innerBlocks", - 0 - ], + "targetRef": "button", "interactionPath": [ "target" ] } - ] -} \ No newline at end of file + ], + "targetRefs": { + "button": { + "gutenberg": { + "blockPath": [ + "innerBlocks", + 0 + ] + }, + "elementor": { + "path": [], + "target": { + "type": "selector", + "value": "a.elementor-button" + } + }, + "bricks": { + "id": "interact-bouncing-button" + }, + "divi": { + "id": "interact-bouncing-button" + } + } + }, + "gutenbergExample": "\r\n
\r\n\r\n
\r\n", + "elementorExample": [ + { + "id": "interact-bouncing-button", + "elType": "widget", + "widgetType": "button", + "settings": { + "text": "Button" + } + } + ], + "bricksExample": [ + { + "id": "interact-bouncing-button", + "name": "button", + "settings": { + "text": "Button", + "style": "primary" + } + } + ], + "diviExample": { + "id": "interact-bouncing-button", + "name": "divi/button", + "props": { + "attrs": { + "button": { + "innerContent": { + "desktop": { + "value": { + "text": "Button" + } + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/button-underline.json b/src/editor/interaction-library/library/button-underline.json index ac17be3..86b65ba 100644 --- a/src/editor/interaction-library/library/button-underline.json +++ b/src/editor/interaction-library/library/button-underline.json @@ -4,4 +4,4 @@ "description": "Buttons with underline on hover", "category": "button", "preview": "buttonUnderline" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-1.json b/src/editor/interaction-library/library/card-1.json index 6941a49..1f94422 100644 --- a/src/editor/interaction-library/library/card-1.json +++ b/src/editor/interaction-library/library/card-1.json @@ -4,4 +4,4 @@ "description": "Card with a see more", "category": "card", "preview": "card1" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-2.json b/src/editor/interaction-library/library/card-2.json index eecda29..1bce821 100644 --- a/src/editor/interaction-library/library/card-2.json +++ b/src/editor/interaction-library/library/card-2.json @@ -4,4 +4,4 @@ "description": "Cards sliding in with rotation effect", "category": "card", "preview": "card2" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-3.json b/src/editor/interaction-library/library/card-3.json index c21641e..fc47409 100644 --- a/src/editor/interaction-library/library/card-3.json +++ b/src/editor/interaction-library/library/card-3.json @@ -4,4 +4,4 @@ "description": "Cards scaling up based on scroll strength", "category": "card", "preview": "card3" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-4.json b/src/editor/interaction-library/library/card-4.json index 92532aa..f36562d 100644 --- a/src/editor/interaction-library/library/card-4.json +++ b/src/editor/interaction-library/library/card-4.json @@ -4,4 +4,4 @@ "description": "A card with an image that rotates up on hover", "category": "card", "preview": "card4" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-5.json b/src/editor/interaction-library/library/card-5.json index b51ef93..3597c7f 100644 --- a/src/editor/interaction-library/library/card-5.json +++ b/src/editor/interaction-library/library/card-5.json @@ -4,4 +4,4 @@ "description": "Cards that show overlay content on hover", "category": "card", "preview": "card5" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-6.json b/src/editor/interaction-library/library/card-6.json index 10f5870..a72b8a2 100644 --- a/src/editor/interaction-library/library/card-6.json +++ b/src/editor/interaction-library/library/card-6.json @@ -4,4 +4,4 @@ "description": "Cards with zooming effect on the background", "category": "card", "preview": "card6" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-7.json b/src/editor/interaction-library/library/card-7.json index 835fcb8..92a9302 100644 --- a/src/editor/interaction-library/library/card-7.json +++ b/src/editor/interaction-library/library/card-7.json @@ -4,4 +4,4 @@ "description": "Horizontal card with zooming image on hover", "category": "card", "preview": "card7" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-8.json b/src/editor/interaction-library/library/card-8.json index d5c0c63..b7df6de 100644 --- a/src/editor/interaction-library/library/card-8.json +++ b/src/editor/interaction-library/library/card-8.json @@ -4,4 +4,4 @@ "description": "Cards that shows additional content on hover", "category": "card", "preview": "card8" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-9.json b/src/editor/interaction-library/library/card-9.json index dfd270e..ed56c86 100644 --- a/src/editor/interaction-library/library/card-9.json +++ b/src/editor/interaction-library/library/card-9.json @@ -4,4 +4,4 @@ "description": "Horizontal cards that flips beneath the next on page scroll", "category": "card", "preview": "card9" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/card-flip.json b/src/editor/interaction-library/library/card-flip.json index 9bc2d0f..f79c873 100644 --- a/src/editor/interaction-library/library/card-flip.json +++ b/src/editor/interaction-library/library/card-flip.json @@ -4,4 +4,4 @@ "description": "Flips a card horizontally", "category": "card", "preview": "cardFlip" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/columns-fade-in-stagger.json b/src/editor/interaction-library/library/columns-fade-in-stagger.json index 65d228a..d537a3a 100644 --- a/src/editor/interaction-library/library/columns-fade-in-stagger.json +++ b/src/editor/interaction-library/library/columns-fade-in-stagger.json @@ -138,4 +138,4 @@ "\r\n
\r\n
\"\"
\r\n\r\n\r\n\r\n

Verdant

\r\n\r\n\r\n\r\n

Lush greenery symbolizing life and growth. A bold statement of freshness and vitality.

\r\n
\r\n" ], "applyWhitelist": [] -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/columns-slide-in-stagger.json b/src/editor/interaction-library/library/columns-slide-in-stagger.json index 0f354b5..fb747f0 100644 --- a/src/editor/interaction-library/library/columns-slide-in-stagger.json +++ b/src/editor/interaction-library/library/columns-slide-in-stagger.json @@ -225,4 +225,4 @@ "\r\n
\r\n
\"\"
\r\n\r\n\r\n\r\n

Verdant

\r\n\r\n\r\n\r\n

Lush greenery symbolizing life and growth. A bold statement of freshness and vitality.

\r\n
\r\n" ], "applyWhitelist": [] -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/confetti.json b/src/editor/interaction-library/library/confetti.json index eb6fa59..76b637d 100644 --- a/src/editor/interaction-library/library/confetti.json +++ b/src/editor/interaction-library/library/confetti.json @@ -98,16 +98,73 @@ ] } ], - "serializedBlockExample": "\r\n
\r\n\r\n
\r\n", "targetMappings": [ { - "blockPath": [ - "innerBlocks", - 0 - ], + "targetRef": "button", "interactionPath": [ "target" ] } - ] -} \ No newline at end of file + ], + "targetRefs": { + "button": { + "gutenberg": { + "blockPath": [ + "innerBlocks", + 0 + ] + }, + "elementor": { + "path": [], + "target": { + "type": "selector", + "value": "a.elementor-button" + } + }, + "bricks": { + "id": "interact-confetti-button" + }, + "divi": { + "id": "interact-confetti-button" + } + } + }, + "gutenbergExample": "\r\n
\r\n\r\n
\r\n", + "elementorExample": [ + { + "id": "interact-confetti-button", + "elType": "widget", + "widgetType": "button", + "settings": { + "text": "Button" + } + } + ], + "bricksExample": [ + { + "id": "interact-confetti-button", + "name": "button", + "settings": { + "text": "Button", + "style": "primary" + } + } + ], + "diviExample": { + "id": "interact-confetti-button", + "name": "divi/button", + "props": { + "button": { + "innerContent": { + "desktop": { + "value": { + "text": "Button", + "linkUrl": "#", + "linkTarget": "off" + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/content-stagger.json b/src/editor/interaction-library/library/content-stagger.json index 80cc8ce..cf961d7 100644 --- a/src/editor/interaction-library/library/content-stagger.json +++ b/src/editor/interaction-library/library/content-stagger.json @@ -4,4 +4,4 @@ "description": "A column with staggered content", "category": "columns", "preview": "contentStagger" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/copy-text.json b/src/editor/interaction-library/library/copy-text.json index b1993b3..7de7196 100644 --- a/src/editor/interaction-library/library/copy-text.json +++ b/src/editor/interaction-library/library/copy-text.json @@ -7,4 +7,4 @@ "category": "click" }, "preview": "copyText" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/counting-up.json b/src/editor/interaction-library/library/counting-up.json index bdcdc64..f7ee4be 100644 --- a/src/editor/interaction-library/library/counting-up.json +++ b/src/editor/interaction-library/library/counting-up.json @@ -4,4 +4,4 @@ "description": "A card with a number that counts up to the desired value", "category": "card", "preview": "countingUp" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/details-slide-in-open.json b/src/editor/interaction-library/library/details-slide-in-open.json index ad00ed2..882905d 100644 --- a/src/editor/interaction-library/library/details-slide-in-open.json +++ b/src/editor/interaction-library/library/details-slide-in-open.json @@ -4,4 +4,4 @@ "description": "Details with slide animation on open", "category": "details", "preview": "detailsSlideInOpen" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/filling-button.json b/src/editor/interaction-library/library/filling-button.json index d60347d..f7596cd 100644 --- a/src/editor/interaction-library/library/filling-button.json +++ b/src/editor/interaction-library/library/filling-button.json @@ -8,4 +8,4 @@ "category": "click" }, "preview": "fillingButton" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/flickering-text.json b/src/editor/interaction-library/library/flickering-text.json index 55b4756..91d54ef 100644 --- a/src/editor/interaction-library/library/flickering-text.json +++ b/src/editor/interaction-library/library/flickering-text.json @@ -183,5 +183,37 @@ ] } ], - "serializedBlockExample": "\r\n

Interactions

\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n

Interactions

\r\n", + "elementorExample": [ + { + "id": "interact-flickering-text", + "elType": "widget", + "widgetType": "text-editor", + "settings": { + "editor": "

Interactions

" + } + } + ], + "bricksExample": [ + { + "id": "interact-flickering-text", + "name": "heading", + "settings": { + "text": "Interactions" + } + } + ], + "diviExample": { + "id": "interact-flickering-text", + "name": "divi/text", + "props": { + "content": { + "innerContent": { + "desktop": { + "value": "

Interactions

" + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/glowing-button.json b/src/editor/interaction-library/library/glowing-button.json index 7443afc..66e6561 100644 --- a/src/editor/interaction-library/library/glowing-button.json +++ b/src/editor/interaction-library/library/glowing-button.json @@ -8,4 +8,4 @@ "category": "hover" }, "preview": "glowingButton" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/grow-on-scroll.json b/src/editor/interaction-library/library/grow-on-scroll.json index b416dc0..d806329 100644 --- a/src/editor/interaction-library/library/grow-on-scroll.json +++ b/src/editor/interaction-library/library/grow-on-scroll.json @@ -8,4 +8,4 @@ "category": "scroll" }, "preview": "growOnScroll" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/growing-button.json b/src/editor/interaction-library/library/growing-button.json index 49fbffb..efe47e6 100644 --- a/src/editor/interaction-library/library/growing-button.json +++ b/src/editor/interaction-library/library/growing-button.json @@ -167,16 +167,75 @@ ] } ], - "serializedBlockExample": "\r\n
\r\n\r\n
\r\n", "targetMappings": [ { - "blockPath": [ - "innerBlocks", - 0 - ], + "targetRef": "button", "interactionPath": [ "target" ] } - ] -} \ No newline at end of file + ], + "targetRefs": { + "button": { + "gutenberg": { + "blockPath": [ + "innerBlocks", + 0 + ] + }, + "elementor": { + "path": [], + "target": { + "type": "selector", + "value": "a.elementor-button" + } + }, + "bricks": { + "id": "interact-growing-button" + }, + "divi": { + "id": "interact-growing-button" + } + } + }, + "gutenbergExample": "\r\n
\r\n\r\n
\r\n", + "elementorExample": [ + { + "id": "interact-growing-button", + "elType": "widget", + "widgetType": "button", + "settings": { + "text": "Button" + } + } + ], + "bricksExample": [ + { + "id": "interact-growing-button", + "name": "button", + "settings": { + "text": "Button", + "style": "primary" + } + } + ], + "diviExample": { + "id": "interact-growing-button", + "name": "divi/button", + "props": { + "attrs": { + "button": { + "innerContent": { + "desktop": { + "value": { + "text": "Button", + "linkUrl": "#", + "linkTarget": "off" + } + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/hero-1.json b/src/editor/interaction-library/library/hero-1.json index 17be341..4053e0a 100644 --- a/src/editor/interaction-library/library/hero-1.json +++ b/src/editor/interaction-library/library/hero-1.json @@ -402,23 +402,42 @@ ], "options": {} }, - "serializedBlockExample": "\r\n
\r\n
\r\n
\r\n

Make Any Website Interactive with the
WordPress Editor

\r\n\r\n\r\n\r\n

Interactions goes beyond animations,
offering versatile trigger and action capabilities.

\r\n\r\n\r\n\r\n
\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n

How it Works

\r\n\r\n\r\n\r\n

With one trigger, you can set off a series of actions that you can play

\r\n\r\n\r\n\r\n

A trigger can be a click, hover, page scroll, page state, etc; and then you can build a timeline of actions like fade, move, scroll, update post meta data, call webhooks, and more. Combine and build experiences.

\r\n
\r\n\r\n\r\n\r\n
\r\n
\"\"
\r\n
\r\n\r\n\r\n\r\n
\r\n
\"\"
\r\n
\r\n
\r\n
\r\n", + "targetRefs": { + "heroRoot": [], + "heroVideo": [ + "innerBlocks", + 0, + "innerBlocks", + 1, + "innerBlocks", + 0 + ], + "featureImageOne": [ + "innerBlocks", + 2, + "innerBlocks", + 1, + "innerBlocks", + 0 + ], + "featureImageTwo": [ + "innerBlocks", + 2, + "innerBlocks", + 2, + "innerBlocks", + 0 + ] + }, "targetMappings": [ { - "blockPath": [], + "targetRef": "heroRoot", "interactionPath": [ "target" ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -428,14 +447,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -445,14 +457,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "featureImageOne", "interactionPath": [ "timelines", 0, @@ -462,14 +467,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "featureImageOne", "interactionPath": [ "timelines", 0, @@ -479,14 +477,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 2, - "innerBlocks", - 0 - ], + "targetRef": "featureImageTwo", "interactionPath": [ "timelines", 0, @@ -496,14 +487,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 2, - "innerBlocks", - 0 - ], + "targetRef": "featureImageTwo", "interactionPath": [ "timelines", 0, @@ -513,14 +497,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -530,14 +507,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -547,14 +517,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -564,14 +527,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "featureImageOne", "interactionPath": [ "timelines", 0, @@ -581,14 +537,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "featureImageOne", "interactionPath": [ "timelines", 0, @@ -598,14 +547,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 2, - "innerBlocks", - 0 - ], + "targetRef": "featureImageTwo", "interactionPath": [ "timelines", 0, @@ -615,14 +557,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -632,14 +567,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -649,14 +577,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 0, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "heroVideo", "interactionPath": [ "timelines", 0, @@ -666,14 +587,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "featureImageOne", "interactionPath": [ "timelines", 0, @@ -683,14 +597,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 1, - "innerBlocks", - 0 - ], + "targetRef": "featureImageOne", "interactionPath": [ "timelines", 0, @@ -700,14 +607,7 @@ ] }, { - "blockPath": [ - "innerBlocks", - 2, - "innerBlocks", - 2, - "innerBlocks", - 0 - ], + "targetRef": "featureImageTwo", "interactionPath": [ "timelines", 0, @@ -717,5 +617,6 @@ ] } ], - "applyWhitelist": [] -} \ No newline at end of file + "applyWhitelist": [], + "gutenbergExample": "\r\n
\r\n
\r\n
\r\n

Make Any Website Interactive with the
WordPress Editor

\r\n\r\n\r\n\r\n

Interactions goes beyond animations,
offering versatile trigger and action capabilities.

\r\n\r\n\r\n\r\n
\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n

How it Works

\r\n\r\n\r\n\r\n

With one trigger, you can set off a series of actions that you can play

\r\n\r\n\r\n\r\n

A trigger can be a click, hover, page scroll, page state, etc; and then you can build a timeline of actions like fade, move, scroll, update post meta data, call webhooks, and more. Combine and build experiences.

\r\n
\r\n\r\n\r\n\r\n
\r\n
\"\"
\r\n
\r\n\r\n\r\n\r\n
\r\n
\"\"
\r\n
\r\n
\r\n
\r\n" +} diff --git a/src/editor/interaction-library/library/hero-2.json b/src/editor/interaction-library/library/hero-2.json index f765b28..ff0c88e 100644 --- a/src/editor/interaction-library/library/hero-2.json +++ b/src/editor/interaction-library/library/hero-2.json @@ -4,4 +4,4 @@ "description": "Hero with a background video that plays along with the amount of scrolling.", "category": "hero", "preview": "hero2" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/hero-3.json b/src/editor/interaction-library/library/hero-3.json index f340ddc..3ff21d3 100644 --- a/src/editor/interaction-library/library/hero-3.json +++ b/src/editor/interaction-library/library/hero-3.json @@ -114,6 +114,6 @@ ], "options": [] }, - "serializedBlockExample": "\r\n
\r\n

Modern Web Solutions\r\nBuilt for Tomorrow's\r\nDigital Needs

\r\n
\r\n", - "applyWhitelist": [] -} \ No newline at end of file + "applyWhitelist": [], + "gutenbergExample": "\r\n
\r\n

Modern Web Solutions\r\nBuilt for Tomorrow's\r\nDigital Needs

\r\n
\r\n" +} diff --git a/src/editor/interaction-library/library/hero-4.json b/src/editor/interaction-library/library/hero-4.json index d0892f5..d1d189d 100644 --- a/src/editor/interaction-library/library/hero-4.json +++ b/src/editor/interaction-library/library/hero-4.json @@ -250,6 +250,6 @@ "resetDelay": "0" } }, - "serializedBlockExample": "\r\n
\r\n

Welcome to Your Next Big Thing

\r\n\r\n\r\n\r\n

This is a short description that highlights your product, service, or brand. Keep it concise, engaging, and focused on the value you provide.

\r\n\r\n\r\n\r\n
\r\n\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n
\r\n", - "applyWhitelist": [] -} \ No newline at end of file + "applyWhitelist": [], + "gutenbergExample": "\r\n
\r\n

Welcome to Your Next Big Thing

\r\n\r\n\r\n\r\n

This is a short description that highlights your product, service, or brand. Keep it concise, engaging, and focused on the value you provide.

\r\n\r\n\r\n\r\n
\r\n\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n\r\n\r\n\r\n
\r\n
\r\n
\r\n
\r\n" +} diff --git a/src/editor/interaction-library/library/hero-5.json b/src/editor/interaction-library/library/hero-5.json index 45b96a6..127c7a7 100644 --- a/src/editor/interaction-library/library/hero-5.json +++ b/src/editor/interaction-library/library/hero-5.json @@ -193,6 +193,6 @@ ], "options": [] }, - "serializedBlockExample": "\r\n
\r\n
\r\n

2000-2010

\r\n\r\n\r\n\r\n

Designing Spaces That Inspire

\r\n\r\n\r\n\r\n

BY INTERACTIONS

\r\n\r\n\r\n\r\n

We bring vision, creativity, and precision together to craft architectural solutions that blend aesthetics with functionality. From concept to completion, our team ensures every project is built to stand the test of time while reflecting the unique character of its environment.

\r\n
\r\n\r\n\r\n\r\n
\r\n
\"\"
\r\n
\r\n
\r\n", - "applyWhitelist": [] -} \ No newline at end of file + "applyWhitelist": [], + "gutenbergExample": "\r\n
\r\n
\r\n

2000-2010

\r\n\r\n\r\n\r\n

Designing Spaces That Inspire

\r\n\r\n\r\n\r\n

BY INTERACTIONS

\r\n\r\n\r\n\r\n

We bring vision, creativity, and precision together to craft architectural solutions that blend aesthetics with functionality. From concept to completion, our team ensures every project is built to stand the test of time while reflecting the unique character of its environment.

\r\n
\r\n\r\n\r\n\r\n
\r\n
\"\"
\r\n
\r\n
\r\n" +} diff --git a/src/editor/interaction-library/library/hero-6.json b/src/editor/interaction-library/library/hero-6.json index b610250..0db281c 100644 --- a/src/editor/interaction-library/library/hero-6.json +++ b/src/editor/interaction-library/library/hero-6.json @@ -4,4 +4,4 @@ "description": "Hero with large headings and subtitles, appearing one by one.", "category": "hero", "preview": "hero6" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/hero-7.json b/src/editor/interaction-library/library/hero-7.json index 6f50ae1..a1d05b3 100644 --- a/src/editor/interaction-library/library/hero-7.json +++ b/src/editor/interaction-library/library/hero-7.json @@ -4,4 +4,4 @@ "description": "Hero with scaling down background, and headings sliding in.", "category": "hero", "preview": "hero7" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/hero-8.json b/src/editor/interaction-library/library/hero-8.json index 7183f64..e55b410 100644 --- a/src/editor/interaction-library/library/hero-8.json +++ b/src/editor/interaction-library/library/hero-8.json @@ -4,4 +4,4 @@ "description": "Hero with content parallax scrolling effect", "category": "hero", "preview": "hero8" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/hero-9.json b/src/editor/interaction-library/library/hero-9.json index d5902e0..09ae94f 100644 --- a/src/editor/interaction-library/library/hero-9.json +++ b/src/editor/interaction-library/library/hero-9.json @@ -4,4 +4,4 @@ "description": "Hero with parallax scrolling effect on the succeeding content", "category": "hero", "preview": "hero9" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/image-3d-mouse-tilt.json b/src/editor/interaction-library/library/image-3d-mouse-tilt.json index c540423..5998ab4 100644 --- a/src/editor/interaction-library/library/image-3d-mouse-tilt.json +++ b/src/editor/interaction-library/library/image-3d-mouse-tilt.json @@ -8,4 +8,4 @@ "category": "hover" }, "preview": "image3DMouseTilt" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/image-blur.json b/src/editor/interaction-library/library/image-blur.json index f86b6ac..6cb885a 100644 --- a/src/editor/interaction-library/library/image-blur.json +++ b/src/editor/interaction-library/library/image-blur.json @@ -148,8 +148,55 @@ ] } ], - "serializedBlockExample": "\r\n
\"\"
\r\n", "applyWhitelist": [ "\\bimage\\b" - ] -} \ No newline at end of file + ], + "gutenbergExample": "\r\n
\"\"
\r\n", + "elementorExample": [ + { + "id": "interact-image-blur", + "elType": "widget", + "widgetType": "image", + "settings": { + "image": { + "id": "", + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "source": "url", + "size": "" + }, + "image_size": "full", + "caption_source": "none", + "link_to": "none", + "open_lightbox": "default" + } + } + ], + "bricksExample": [ + { + "id": "interact-image-blur", + "name": "image", + "settings": { + "image": { + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg" + } + } + } + ], + "diviExample": { + "id": "interact-image-blur", + "name": "divi/image", + "props": { + "image": { + "innerContent": { + "desktop": { + "value": { + "src": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "alt": "", + "titleText": "" + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/image-fade-in-entrance.json b/src/editor/interaction-library/library/image-fade-in-entrance.json index 23db81a..45fd9ac 100644 --- a/src/editor/interaction-library/library/image-fade-in-entrance.json +++ b/src/editor/interaction-library/library/image-fade-in-entrance.json @@ -95,5 +95,52 @@ ] } ], - "serializedBlockExample": "\r\n
\"\"
\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n
\"\"
\r\n", + "elementorExample": [ + { + "id": "interact-image-fade-in-entrance", + "elType": "widget", + "widgetType": "image", + "settings": { + "image": { + "id": "", + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "source": "url", + "size": "" + }, + "image_size": "full", + "caption_source": "none", + "link_to": "none", + "open_lightbox": "default" + } + } + ], + "bricksExample": [ + { + "id": "interact-image-fade-in-entrance", + "name": "image", + "settings": { + "image": { + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg" + } + } + } + ], + "diviExample": { + "id": "interact-image-fade-in-entrance", + "name": "divi/image", + "props": { + "image": { + "innerContent": { + "desktop": { + "value": { + "src": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "alt": "", + "titleText": "" + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/image-slide-in-entrance.json b/src/editor/interaction-library/library/image-slide-in-entrance.json index 2895c08..b3e1475 100644 --- a/src/editor/interaction-library/library/image-slide-in-entrance.json +++ b/src/editor/interaction-library/library/image-slide-in-entrance.json @@ -178,5 +178,52 @@ ] } ], - "serializedBlockExample": "\r\n
\"\"
\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n
\"\"
\r\n", + "elementorExample": [ + { + "id": "interact-image-slide-in-entrance", + "elType": "widget", + "widgetType": "image", + "settings": { + "image": { + "id": "", + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "source": "url", + "size": "" + }, + "image_size": "full", + "caption_source": "none", + "link_to": "none", + "open_lightbox": "default" + } + } + ], + "bricksExample": [ + { + "id": "interact-image-slide-in-entrance", + "name": "image", + "settings": { + "image": { + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg" + } + } + } + ], + "diviExample": { + "id": "interact-image-slide-in-entrance", + "name": "divi/image", + "props": { + "image": { + "innerContent": { + "desktop": { + "value": { + "src": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "alt": "", + "titleText": "" + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/image-zoom-out-rotate.json b/src/editor/interaction-library/library/image-zoom-out-rotate.json index a3db556..16a340b 100644 --- a/src/editor/interaction-library/library/image-zoom-out-rotate.json +++ b/src/editor/interaction-library/library/image-zoom-out-rotate.json @@ -56,7 +56,7 @@ "rotate": "10" } }, - { + { "type": "scale", "target": { "type": "selector", @@ -260,8 +260,55 @@ ] } ], - "serializedBlockExample": "\r\n
\"\"/
\r\n", "applyWhitelist": [ "\\bimage\\b" - ] -} \ No newline at end of file + ], + "gutenbergExample": "\r\n
\"\"/
\r\n", + "elementorExample": [ + { + "id": "interact-image-zoom-out-rotate", + "elType": "widget", + "widgetType": "image", + "settings": { + "image": { + "id": "", + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "source": "url", + "size": "" + }, + "image_size": "full", + "caption_source": "none", + "link_to": "none", + "open_lightbox": "default" + } + } + ], + "bricksExample": [ + { + "id": "interact-image-zoom-out-rotate", + "name": "image", + "settings": { + "image": { + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg" + } + } + } + ], + "diviExample": { + "id": "interact-image-zoom-out-rotate", + "name": "divi/image", + "props": { + "image": { + "innerContent": { + "desktop": { + "value": { + "src": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "alt": "", + "titleText": "" + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/image-zoom.json b/src/editor/interaction-library/library/image-zoom.json index af0adce..e2b4640 100644 --- a/src/editor/interaction-library/library/image-zoom.json +++ b/src/editor/interaction-library/library/image-zoom.json @@ -204,8 +204,55 @@ ] } ], - "serializedBlockExample": "\r\n
\"\"/
\r\n", "applyWhitelist": [ "\\bimage\\b" - ] -} \ No newline at end of file + ], + "gutenbergExample": "\r\n
\"\"/
\r\n", + "elementorExample": [ + { + "id": "interact-image-zoom", + "elType": "widget", + "widgetType": "image", + "settings": { + "image": { + "id": "", + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "source": "url", + "size": "" + }, + "image_size": "full", + "caption_source": "none", + "link_to": "none", + "open_lightbox": "default" + } + } + ], + "bricksExample": [ + { + "id": "interact-image-zoom", + "name": "image", + "settings": { + "image": { + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg" + } + } + } + ], + "diviExample": { + "id": "interact-image-zoom", + "name": "divi/image", + "props": { + "image": { + "innerContent": { + "desktop": { + "value": { + "src": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "alt": "", + "titleText": "" + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/logo-carousel.json b/src/editor/interaction-library/library/logo-carousel.json index c01ffd2..758d94b 100644 --- a/src/editor/interaction-library/library/logo-carousel.json +++ b/src/editor/interaction-library/library/logo-carousel.json @@ -4,4 +4,4 @@ "description": "Logos sliding horizontally back and forth", "category": "columns", "preview": "logoCarousel" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/parallax.json b/src/editor/interaction-library/library/parallax.json index e15956b..397e774 100644 --- a/src/editor/interaction-library/library/parallax.json +++ b/src/editor/interaction-library/library/parallax.json @@ -129,4 +129,4 @@ "\r\n
\r\n
\"\"
\r\n
\r\n" ], "applyWhitelist": [] -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/play-video.json b/src/editor/interaction-library/library/play-video.json index 7d00789..b789521 100644 --- a/src/editor/interaction-library/library/play-video.json +++ b/src/editor/interaction-library/library/play-video.json @@ -127,9 +127,9 @@ ] } ], - "serializedBlockExample": "\n
\n", "applyWhitelist": [ "/video", "core/cover" - ] -} \ No newline at end of file + ], + "gutenbergExample": "\n
\n" +} diff --git a/src/editor/interaction-library/library/price-calculator.json b/src/editor/interaction-library/library/price-calculator.json index 3ec527b..d147eb3 100644 --- a/src/editor/interaction-library/library/price-calculator.json +++ b/src/editor/interaction-library/library/price-calculator.json @@ -5,4 +5,4 @@ "category": "calculator", "preview": "priceCalculator", "config": "priceCalculator" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/pulsing-button.json b/src/editor/interaction-library/library/pulsing-button.json index 64b1771..76af657 100644 --- a/src/editor/interaction-library/library/pulsing-button.json +++ b/src/editor/interaction-library/library/pulsing-button.json @@ -143,16 +143,75 @@ ] } ], - "serializedBlockExample": "\r\n
\r\n\r\n
\r\n", "targetMappings": [ { - "blockPath": [ - "innerBlocks", - 0 - ], + "targetRef": "button", "interactionPath": [ "target" ] } - ] -} \ No newline at end of file + ], + "targetRefs": { + "button": { + "gutenberg": { + "blockPath": [ + "innerBlocks", + 0 + ] + }, + "elementor": { + "path": [], + "target": { + "type": "selector", + "value": "a.elementor-button" + } + }, + "bricks": { + "id": "interact-pulsing-button" + }, + "divi": { + "id": "interact-pulsing-button" + } + } + }, + "gutenbergExample": "\r\n
\r\n\r\n
\r\n", + "elementorExample": [ + { + "id": "interact-pulsing-button", + "elType": "widget", + "widgetType": "button", + "settings": { + "text": "Button" + } + } + ], + "bricksExample": [ + { + "id": "interact-pulsing-button", + "name": "button", + "settings": { + "text": "Button", + "style": "primary" + } + } + ], + "diviExample": { + "id": "interact-pulsing-button", + "name": "divi/button", + "props": { + "attrs": { + "button": { + "innerContent": { + "desktop": { + "value": { + "text": "Button", + "linkUrl": "#", + "linkTarget": "off" + } + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/revealing-text.json b/src/editor/interaction-library/library/revealing-text.json index 6c82924..b9159ca 100644 --- a/src/editor/interaction-library/library/revealing-text.json +++ b/src/editor/interaction-library/library/revealing-text.json @@ -105,9 +105,9 @@ ], "options": [] }, - "serializedBlockExample": "\r\n
\r\n
\r\n

Experience the Magic of Flight Above the Clouds

\r\n\r\n\r\n\r\n
\"\"
\r\n
\r\n
\r\n", "targetMappings": [ { + "targetRef": "image", "blockPath": [ "innerBlocks", 0, @@ -119,5 +119,129 @@ ] } ], - "applyWhitelist": [] -} \ No newline at end of file + "targetRefs": { + "image": { + "gutenberg": { + "blockPath": [ + "innerBlocks", + 0, + "innerBlocks", + 1 + ] + }, + "elementor": { + "path": [ + 0, + "elements", + 1 + ] + }, + "bricks": { + "id": "interact-revealing-text-image" + }, + "divi": { + "id": "interact-revealing-text-image" + } + } + }, + "applyWhitelist": [], + "gutenbergExample": "\r\n
\r\n
\r\n

Experience the Magic of Flight Above the Clouds

\r\n\r\n\r\n\r\n
\"\"
\r\n
\r\n
\r\n", + "elementorExample": [ + { + "id": "interact-revealing-text-container", + "elType": "container", + "settings": { + "content_width": "full" + }, + "elements": [ + { + "id": "interact-revealing-text-heading", + "elType": "widget", + "widgetType": "heading", + "settings": { + "title": "Experience the Magic of Flight Above the Clouds", + "header_size": "h1", + "align": "center" + } + }, + { + "id": "interact-revealing-text-image", + "elType": "widget", + "widgetType": "image", + "settings": { + "image": { + "id": "", + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "source": "url", + "size": "" + }, + "image_size": "full", + "caption_source": "none", + "link_to": "none", + "open_lightbox": "default" + } + } + ] + } + ], + "bricksExample": [ + { + "id": "interact-revealing-text-container", + "name": "container", + "children": [ + { + "id": "interact-revealing-text-heading", + "name": "heading", + "settings": { + "text": "Experience the Magic of Flight Above the Clouds" + } + }, + { + "id": "interact-revealing-text-image", + "name": "image", + "settings": { + "image": { + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg" + } + } + } + ] + } + ], + "diviExample": { + "id": "interact-revealing-text-container", + "name": "divi/group", + "children": [ + { + "id": "interact-revealing-text-heading", + "name": "divi/text", + "props": { + "content": { + "innerContent": { + "desktop": { + "value": "

Experience the Magic of Flight Above the Clouds

" + } + } + } + } + }, + { + "id": "interact-revealing-text-image", + "name": "divi/image", + "props": { + "image": { + "innerContent": { + "desktop": { + "value": { + "src": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "alt": "", + "titleText": "" + } + } + } + } + } + } + ] + } +} diff --git a/src/editor/interaction-library/library/rotating-image.json b/src/editor/interaction-library/library/rotating-image.json index daa374c..d1a0bed 100644 --- a/src/editor/interaction-library/library/rotating-image.json +++ b/src/editor/interaction-library/library/rotating-image.json @@ -150,5 +150,52 @@ ] } ], - "serializedBlockExample": "\r\n
\r\n\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n
\r\n\r\n", + "elementorExample": [ + { + "id": "interact-rotating-image", + "elType": "widget", + "widgetType": "image", + "settings": { + "image": { + "id": "", + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "source": "url", + "size": "" + }, + "image_size": "full", + "caption_source": "none", + "link_to": "none", + "open_lightbox": "default" + } + } + ], + "bricksExample": [ + { + "id": "interact-rotating-image", + "name": "image", + "settings": { + "image": { + "url": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg" + } + } + } + ], + "diviExample": { + "id": "interact-rotating-image", + "name": "divi/image", + "props": { + "image": { + "innerContent": { + "desktop": { + "value": { + "src": "https://images.pexels.com/photos/2325452/pexels-photo-2325452.jpeg?cs=srgb&dl=pexels-francesco-ungaro-2325452.jpg", + "alt": "", + "titleText": "" + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/scroll-to-top.json b/src/editor/interaction-library/library/scroll-to-top.json index 9ee0958..4f45e6d 100644 --- a/src/editor/interaction-library/library/scroll-to-top.json +++ b/src/editor/interaction-library/library/scroll-to-top.json @@ -8,4 +8,4 @@ "category": "click" }, "preview": "scrollToTop" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/scrub-video.json b/src/editor/interaction-library/library/scrub-video.json index 7e4485e..d7f73b1 100644 --- a/src/editor/interaction-library/library/scrub-video.json +++ b/src/editor/interaction-library/library/scrub-video.json @@ -4,4 +4,4 @@ "description": "While scrolling, the background video plays along with the amount of scroll", "category": "video", "preview": "scrubVideo" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/shaking-button.json b/src/editor/interaction-library/library/shaking-button.json index 04c5369..0797e3c 100644 --- a/src/editor/interaction-library/library/shaking-button.json +++ b/src/editor/interaction-library/library/shaking-button.json @@ -225,16 +225,75 @@ ] } ], - "serializedBlockExample": "\r\n
\r\n\r\n
\r\n", "targetMappings": [ { - "blockPath": [ - "innerBlocks", - 0 - ], + "targetRef": "button", "interactionPath": [ "target" ] } - ] -} \ No newline at end of file + ], + "targetRefs": { + "button": { + "gutenberg": { + "blockPath": [ + "innerBlocks", + 0 + ] + }, + "elementor": { + "path": [], + "target": { + "type": "selector", + "value": "a.elementor-button" + } + }, + "bricks": { + "id": "interact-shaking-button" + }, + "divi": { + "id": "interact-shaking-button" + } + } + }, + "gutenbergExample": "\r\n
\r\n\r\n
\r\n", + "elementorExample": [ + { + "id": "interact-shaking-button", + "elType": "widget", + "widgetType": "button", + "settings": { + "text": "Button" + } + } + ], + "bricksExample": [ + { + "id": "interact-shaking-button", + "name": "button", + "settings": { + "text": "Button", + "style": "primary" + } + } + ], + "diviExample": { + "id": "interact-shaking-button", + "name": "divi/button", + "props": { + "attrs": { + "button": { + "innerContent": { + "desktop": { + "value": { + "text": "Button", + "linkUrl": "#", + "linkTarget": "off" + } + } + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/sliding-text.json b/src/editor/interaction-library/library/sliding-text.json index f299a31..4051d97 100644 --- a/src/editor/interaction-library/library/sliding-text.json +++ b/src/editor/interaction-library/library/sliding-text.json @@ -4,4 +4,4 @@ "description": "Staggered sliding text", "category": "text", "preview": "slidingText" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/library/spreading-text.json b/src/editor/interaction-library/library/spreading-text.json index 7359c04..933d713 100644 --- a/src/editor/interaction-library/library/spreading-text.json +++ b/src/editor/interaction-library/library/spreading-text.json @@ -173,5 +173,37 @@ ] } ], - "serializedBlockExample": "\r\n

Interactions

\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n

Interactions

\r\n", + "elementorExample": [ + { + "id": "interact-spreading-text", + "elType": "widget", + "widgetType": "text-editor", + "settings": { + "editor": "

Interactions

" + } + } + ], + "bricksExample": [ + { + "id": "interact-spreading-text", + "name": "heading", + "settings": { + "text": "Interactions" + } + } + ], + "diviExample": { + "id": "interact-spreading-text", + "name": "divi/text", + "props": { + "content": { + "innerContent": { + "desktop": { + "value": "

Interactions

" + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/text-fade-in-entrance.json b/src/editor/interaction-library/library/text-fade-in-entrance.json index c780fe9..4d135f6 100644 --- a/src/editor/interaction-library/library/text-fade-in-entrance.json +++ b/src/editor/interaction-library/library/text-fade-in-entrance.json @@ -83,7 +83,7 @@ "timelines", 0, "actions", - 2, + 0, "timing", "duration" ] @@ -101,5 +101,37 @@ ] } ], - "serializedBlockExample": "\r\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula. Integer mollis quam vel euismod auctor. Donec et velit blandit, pharetra nisl et, cursus leo. Duis hendrerit leo vel nisl consectetur, eget fringilla nisi mattis. Sed ac libero luctus, condimentum ligula ac, laoreet arcu. Duis congue diam et consequat lobortis.

\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula. Integer mollis quam vel euismod auctor. Donec et velit blandit, pharetra nisl et, cursus leo. Duis hendrerit leo vel nisl consectetur, eget fringilla nisi mattis. Sed ac libero luctus, condimentum ligula ac, laoreet arcu. Duis congue diam et consequat lobortis.

\r\n", + "elementorExample": [ + { + "id": "interact-text-fade-in-entrance", + "elType": "widget", + "widgetType": "text-editor", + "settings": { + "editor": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula.

" + } + } + ], + "bricksExample": [ + { + "id": "interact-text-fade-in-entrance", + "name": "text-basic", + "settings": { + "text": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula.

" + } + } + ], + "diviExample": { + "id": "interact-text-fade-in-entrance", + "name": "divi/text", + "props": { + "content": { + "innerContent": { + "desktop": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula.

" + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/text-slide-in-entrance.json b/src/editor/interaction-library/library/text-slide-in-entrance.json index 3b2a440..2569603 100644 --- a/src/editor/interaction-library/library/text-slide-in-entrance.json +++ b/src/editor/interaction-library/library/text-slide-in-entrance.json @@ -175,5 +175,37 @@ ] } ], - "serializedBlockExample": "\r\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula. Integer mollis quam vel euismod auctor. Donec et velit blandit, pharetra nisl et, cursus leo. Duis hendrerit leo vel nisl consectetur, eget fringilla nisi mattis. Sed ac libero luctus, condimentum ligula ac, laoreet arcu. Duis congue diam et consequat lobortis.

\r\n" -} \ No newline at end of file + "gutenbergExample": "\r\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula. Integer mollis quam vel euismod auctor. Donec et velit blandit, pharetra nisl et, cursus leo. Duis hendrerit leo vel nisl consectetur, eget fringilla nisi mattis. Sed ac libero luctus, condimentum ligula ac, laoreet arcu. Duis congue diam et consequat lobortis.

\r\n", + "elementorExample": [ + { + "id": "interact-text-slide-in-entrance", + "elType": "widget", + "widgetType": "text-editor", + "settings": { + "editor": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula.

" + } + } + ], + "bricksExample": [ + { + "id": "interact-text-slide-in-entrance", + "name": "text-basic", + "settings": { + "text": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula.

" + } + } + ], + "diviExample": { + "id": "interact-text-slide-in-entrance", + "name": "divi/text", + "props": { + "content": { + "innerContent": { + "desktop": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel consectetur odio, at sagittis lectus. In at sem et lectus imperdiet vehicula.

" + } + } + } + } + } +} diff --git a/src/editor/interaction-library/library/toggle-details.json b/src/editor/interaction-library/library/toggle-details.json index 2e2aa57..1cf9390 100644 --- a/src/editor/interaction-library/library/toggle-details.json +++ b/src/editor/interaction-library/library/toggle-details.json @@ -4,4 +4,4 @@ "description": "Toggle all details in the container", "category": "details", "preview": "toggleDetails" -} \ No newline at end of file +} diff --git a/src/editor/interaction-library/preset-schema.js b/src/editor/interaction-library/preset-schema.js new file mode 100644 index 0000000..0d22b97 --- /dev/null +++ b/src/editor/interaction-library/preset-schema.js @@ -0,0 +1,76 @@ +const LEGACY_BUILDER_EXAMPLE_KEYS = { + gutenberg: 'serializedBlockExample', + elementor: 'elementorExample', + bricks: 'bricksExample', + divi: 'diviExample', +} + +/** + * Return the builder-specific example payload for a preset. + * + * Supports the finalized top-level `Example` fields while still + * falling back to the older shapes during the migration period. + * + * @param {Object} preset Preset definition. + * @param {string} builder Builder slug, for example `elementor`. + * + * @return {*} Builder-specific example payload. + */ +export const getPresetBuilderExample = ( preset = {}, builder = '' ) => { + const legacyExampleKey = + LEGACY_BUILDER_EXAMPLE_KEYS[ builder ] || `${ builder }Example` + + if ( builder === 'gutenberg' && preset?.gutenbergExample ) { + return preset.gutenbergExample + } + + if ( builder !== 'gutenberg' && preset?.[ `${ builder }Example` ] ) { + return preset[ `${ builder }Example` ] + } + + if ( preset?.builderExamples?.[ builder ] ) { + return preset.builderExamples[ builder ] + } + + if ( legacyExampleKey && preset?.[ legacyExampleKey ] ) { + return preset[ legacyExampleKey ] + } + + return null +} + +/** + * Resolve builder-aware target refs into the shape expected by the active + * editor adapter. + * + * A target ref can either stay generic: + * `button: [ "innerBlocks", 0 ]` + * or provide per-builder data: + * `button: { gutenberg: { blockPath: [...] }, elementor: { path: [...] } }` + * + * @param {Object} preset Preset definition. + * @param {string} builder Builder slug, for example `gutenberg` or `elementor`. + * + * @return {Object} Normalized target refs for the requested builder. + */ +export const getPresetBuilderTargetRefs = ( preset = {}, builder = '' ) => { + const targetRefs = preset?.targetRefs + if ( ! targetRefs || typeof targetRefs !== 'object' ) { + return {} + } + + return Object.entries( targetRefs ).reduce( ( resolvedTargetRefs, [ targetRef, targetRefConfig ] ) => { + if ( + targetRefConfig && + typeof targetRefConfig === 'object' && + ! Array.isArray( targetRefConfig ) && + targetRefConfig[ builder ] + ) { + resolvedTargetRefs[ targetRef ] = targetRefConfig[ builder ] + return resolvedTargetRefs + } + + resolvedTargetRefs[ targetRef ] = targetRefConfig + return resolvedTargetRefs + }, {} ) +} diff --git a/src/editor/interaction-library/select-modal.js b/src/editor/interaction-library/select-modal.js index 717c377..20e7cab 100644 --- a/src/editor/interaction-library/select-modal.js +++ b/src/editor/interaction-library/select-modal.js @@ -18,6 +18,7 @@ import { } from '@wordpress/icons' import { useState, useMemo } from '@wordpress/element' import { __ } from '@wordpress/i18n' +import { isBuilderEditor } from '~interact/editor/editors' /** * Internal deprendencies @@ -37,6 +38,9 @@ export const SelectModal = props => { mode = 'insert', } = props const [ selectedCategory, setSelectedCategory ] = useState( 'all' ) + const incompatibleApplyLabel = isBuilderEditor() + ? __( 'Can not apply to selected element', 'interactions' ) + : __( 'Can not apply to current block', 'interactions' ) const adjustedPresets = useMemo( () => ( presets.map( preset => { @@ -153,7 +157,7 @@ export const SelectModal = props => { } - :

{ __( 'Can not apply to current block', 'interactions' ) }

+ :

{ incompatibleApplyLabel }

} ) } diff --git a/src/editor/interaction-library/util.js b/src/editor/interaction-library/util.js index b8b8400..ebb30d5 100644 --- a/src/editor/interaction-library/util.js +++ b/src/editor/interaction-library/util.js @@ -125,15 +125,107 @@ export const createTargetObj = block => ( { value: getOrGenerateBlockAnchor( block?.clientId ), } ) -// Utility to apply mappings -export const applyTargetMappings = ( interactionSetup, targetMappings, blockOrTarget, fallbackPath = [ 'target' ] ) => { +/** + * Resolve a semantic target ref from a preset to the underlying Gutenberg + * block path used by the inserted block tree. + * + * @param {Object} targetRefs - Preset target ref definitions. + * @param {string|null} targetRef - Semantic ref name to resolve. + * + * @return {?Array} The resolved block path, if available. + */ +const getTargetRefPath = ( targetRefs, targetRef ) => { + if ( ! targetRef || ! targetRefs || typeof targetRefs !== 'object' ) { + return null + } + + const targetRefConfig = targetRefs[ targetRef ] + if ( Array.isArray( targetRefConfig ) ) { + return targetRefConfig + } + + if ( Array.isArray( targetRefConfig?.blockPath ) ) { + return targetRefConfig.blockPath + } + + return null +} + +/** + * Resolve a mapping entry into an interaction target object. + * + * @param {Object} blockOrTarget - Inserted Gutenberg block tree or direct target. + * @param {Object} mapping - Target mapping definition for one assignment. + * @param {Object} targetRefs - Preset target ref definitions. + * @param {?Function} resolver - Optional editor-specific resolver. + * + * @return {?Object} The resolved interaction target object. + */ +const resolveTargetMappingTarget = ( blockOrTarget, mapping = {}, targetRefs = {}, resolver = null ) => { + if ( typeof resolver === 'function' ) { + const resolvedTarget = resolver( mapping ) + if ( resolvedTarget ) { + return resolvedTarget + } + } + + if ( ! blockOrTarget?.clientId ) { + return blockOrTarget + } + + const resolvedBlockPath = Array.isArray( mapping.blockPath ) + ? mapping.blockPath + : getTargetRefPath( targetRefs, mapping.targetRef ) + + if ( ! Array.isArray( resolvedBlockPath ) ) { + return null + } + + const block = getValueAtPath( blockOrTarget, resolvedBlockPath ) + return block?.clientId ? createTargetObj( block ) : null +} + +/** + * Apply preset target mappings to an interaction setup object. + * + * Supports both legacy Gutenberg `blockPath` mappings and semantic `targetRef` + * mappings so presets can migrate gradually without breaking existing inserts. + * + * @param {Object} interactionSetup - Interaction config to mutate. + * @param {Array} targetMappings - Mapping definitions to apply. + * @param {Object} blockOrTarget - Inserted block tree or target. + * @param {Array} fallbackPath - Path used when no mappings exist. + * @param {Object} targetRefs - Preset target ref definitions. + * @param {?Function} resolver - Optional editor-specific resolver. + * + * @return {void} + */ +export const applyTargetMappings = ( + interactionSetup, + targetMappings, + blockOrTarget, + fallbackPath = [ 'target' ], + targetRefs = {}, + resolver = null +) => { // If target mappings are provided, dynamically create target for each. if ( Array.isArray( targetMappings ) && targetMappings.length > 0 ) { - targetMappings.forEach( ( { blockPath, interactionPath } ) => { - // If it has clientId, then it's a block, and we have to create the target object - const target = blockOrTarget?.clientId - ? createTargetObj( getValueAtPath( blockOrTarget, blockPath ) ) - : blockOrTarget + targetMappings.forEach( mapping => { + const { + targetRef, + blockPath, + interactionPath, + } = mapping + const target = resolveTargetMappingTarget( blockOrTarget, mapping, targetRefs, resolver ) + if ( ! target ) { + // eslint-disable-next-line no-console + console.warn( 'Interactions Library target mapping could not be resolved.', { + targetRef, + blockPath, + interactionPath, + } ) + return + } setValueAtPath( interactionSetup, interactionPath, target ) } ) } else {