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..c3ef8fb 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,18 @@ 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' ) + } + return <> { selectedInteraction === null && loadingError && isShowingError && @@ -236,6 +254,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..ff7fcde 100644 --- a/src/editor/editors/abstract.js +++ b/src/editor/editors/abstract.js @@ -6,6 +6,7 @@ import { pluginVersion, srcUrl, } from 'interactions' +import { applyTargetMappings } from '../interaction-library/util' import { select } from '@wordpress/data' const NOOP = () => {} @@ -36,12 +37,16 @@ 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() } ensureBuilderEditorStyles() { @@ -111,6 +116,29 @@ class InteractionsEditorAbstract { return NOOP } + // Persist the parent editor when the interaction data should also be saved. + saveEditor() { + return Promise.resolve() + } + + // Insert a library preset into the current editor and return the inserted + // content descriptor so target mappings can be resolved afterward. + insertLibraryPreset() { + return null + } + + // 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 ) { + applyTargetMappings( + interactionSetup, + selectedPreset.targetMappings, + insertionContext + ) + + 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..8746f33 100644 --- a/src/editor/editors/bricks.js +++ b/src/editor/editors/bricks.js @@ -1,6 +1,7 @@ import IconSVG from '../assets/icon.svg' import InteractionsApp from '../app' import InteractionsEditorAbstract from './abstract' +import { InteractionLibraryRoot } from '../interaction-library' import { __ } from '@wordpress/i18n' import { Button } from '@wordpress/components' @@ -90,6 +91,7 @@ class BricksInteractionsEditor extends InteractionsEditorAbstract { + ) } diff --git a/src/editor/editors/divi.js b/src/editor/editors/divi.js new file mode 100644 index 0000000..23efdae --- /dev/null +++ b/src/editor/editors/divi.js @@ -0,0 +1,429 @@ +import IconSVG from '../assets/icon.svg' +import InteractionsApp from '../app' +import InteractionsEditorAbstract from './abstract' +import { InteractionLibraryRoot } from '../interaction-library' + +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 ) + +// 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 + } + + 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 ) + } + + 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..2c2f790 100644 --- a/src/editor/editors/elementor.js +++ b/src/editor/editors/elementor.js @@ -1,6 +1,7 @@ import IconSVG from '../assets/icon.svg' import InteractionsApp from '../app' import InteractionsEditorAbstract from './abstract' +import { InteractionLibraryRoot } from '../interaction-library' import { __ } from '@wordpress/i18n' import { Button } from '@wordpress/components' @@ -102,6 +103,7 @@ class ElementorInteractionsEditor extends InteractionsEditorAbstract { + ) } diff --git a/src/editor/editors/gutenberg.js b/src/editor/editors/gutenberg.js index 3de9f3a..273282b 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 { applyTargetMappings } from '../interaction-library/util' +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,34 @@ class GutenbergInteractionsEditor extends InteractionsEditorAbstract { options: '', } } + + // Persist the current post when the library or interaction editor saves. + saveEditor() { + return Promise.resolve( dispatch( 'core/editor' )?.savePost?.() ) + } + + // Insert the preset's serialized Gutenberg example and return the inserted + // top-level block so the library can resolve any target mappings from it. + insertLibraryPreset( selectedPreset = {} ) { + const [ block ] = parse( selectedPreset.serializedBlockExample ?? '' ) + if ( ! block ) { + return null + } + + dispatch( 'core/block-editor' ).insertBlocks( block ) + return block + } + + // Resolve target mappings using Gutenberg's current block-tree descriptors. + resolveLibraryPresetTargets( interactionSetup, selectedPreset = {}, insertionContext = null ) { + applyTargetMappings( + interactionSetup, + selectedPreset.targetMappings, + insertionContext + ) + + return interactionSetup + } } 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/configure-modal.js b/src/editor/interaction-library/configure-modal.js index e201bcc..51f1ab2 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 @@ -21,7 +24,6 @@ import { * WordPress deprendencies */ import { Button } from '@wordpress/components' -import { parse } from '@wordpress/blocks' import { dispatch } from '@wordpress/data' import { useState, useMemo, useEffect, @@ -43,10 +45,15 @@ export const ConfigureModal = props => { const [ optionValues, setOptionValues ] = useState( {} ) const [ selectedTarget, setSelectedTarget ] = useState( interactionTarget ) const [ sideBarEl, setSideBarEl ] = useState( null ) + 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 + ) }, [] ) const interactionSetup = useMemo( () => ( selectedPreset.interactionSetup ), [ selectedPreset ] ) @@ -80,21 +87,17 @@ 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 = insertLibraryPreset( selectedPreset ) + if ( ! insertedContent ) { 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 +142,6 @@ export const ConfigureModal = props => { }, } ) ) window?.dispatchEvent( new CustomEvent( 'interact/save-interaction' ) ) - dispatch( 'core/editor' ).savePost() }, 100 ) } else { const newInteraction = createNewInteraction( @@ -209,7 +211,7 @@ export const ConfigureModal = props => { { mode === 'apply' && (
- { __( 'This interaction will be applied to the selected block. Click here to modify.', 'interactions' ) } + { selectedTargetLabel } { ) } + +export const InteractionLibraryRoot = () => { + const interactionLibraryMode = useSelect( select => + select( 'interact/interaction-library-modal' ).getMode(), + [] ) + + return interactionLibraryMode ? : null +} 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 }

} ) }