From 055035cf065a367e883c3b496bbf646eef750c43 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Tue, 28 Apr 2026 10:42:55 +0800 Subject: [PATCH] feat: add stackable integration --- interactions.php | 16 +++++ scripts/generate-frontend-php-scripts.mjs | 46 ++++++++++++++ src/action-types/frontend/backgroundColor.js | 35 ++++++++--- src/action-types/frontend/textColor.js | 31 ++++++++-- src/editor/editor.php | 2 + src/frontend/scripts/class-interaction.js | 55 ++++++++++------- ...action-type-stackable-accordion-toggle.php | 40 +++++++++++++ ...n-type-stackable-carousel-change-slide.php | 39 ++++++++++++ ...s-action-type-stackable-count-up-reset.php | 27 +++++++++ ...e-stackable-horizontal-scroller-scroll.php | 39 ++++++++++++ ...pe-stackable-progress-bar-change-value.php | 39 ++++++++++++ ...stackable-progress-circle-change-value.php | 39 ++++++++++++ ...-action-type-stackable-tabs-change-tab.php | 39 ++++++++++++ .../frontend/stackableAccordionToggle.js | 32 ++++++++++ .../frontend/stackableCarouselChangeSlide.js | 28 +++++++++ .../frontend/stackableCountUpReset.js | 24 ++++++++ .../stackableHorizontalScrollerScroll.js | 40 +++++++++++++ .../stackableProgressBarChangeValue.js | 26 ++++++++ .../stackableProgressCircleChangeValue.js | 26 ++++++++ .../frontend/stackableTabsChangeTab.js | 24 ++++++++ ...action-type-stackable-accordion-toggle.php | 47 +++++++++++++++ ...n-type-stackable-carousel-slide-change.php | 45 ++++++++++++++ ...e-stackable-horizontal-scroller-scroll.php | 45 ++++++++++++++ ...interaction-type-stackable-tabs-change.php | 45 ++++++++++++++ .../frontend/stackableAccordionToggle.js | 36 +++++++++++ .../frontend/stackableCarouselSlideChange.js | 35 +++++++++++ .../stackableHorizontalScrollerScroll.js | 60 +++++++++++++++++++ .../frontend/stackableTabsChange.js | 35 +++++++++++ 28 files changed, 958 insertions(+), 37 deletions(-) create mode 100644 src/integrations/stackable/action-types/class-action-type-stackable-accordion-toggle.php create mode 100644 src/integrations/stackable/action-types/class-action-type-stackable-carousel-change-slide.php create mode 100644 src/integrations/stackable/action-types/class-action-type-stackable-count-up-reset.php create mode 100644 src/integrations/stackable/action-types/class-action-type-stackable-horizontal-scroller-scroll.php create mode 100644 src/integrations/stackable/action-types/class-action-type-stackable-progress-bar-change-value.php create mode 100644 src/integrations/stackable/action-types/class-action-type-stackable-progress-circle-change-value.php create mode 100644 src/integrations/stackable/action-types/class-action-type-stackable-tabs-change-tab.php create mode 100644 src/integrations/stackable/action-types/frontend/stackableAccordionToggle.js create mode 100644 src/integrations/stackable/action-types/frontend/stackableCarouselChangeSlide.js create mode 100644 src/integrations/stackable/action-types/frontend/stackableCountUpReset.js create mode 100644 src/integrations/stackable/action-types/frontend/stackableHorizontalScrollerScroll.js create mode 100644 src/integrations/stackable/action-types/frontend/stackableProgressBarChangeValue.js create mode 100644 src/integrations/stackable/action-types/frontend/stackableProgressCircleChangeValue.js create mode 100644 src/integrations/stackable/action-types/frontend/stackableTabsChangeTab.js create mode 100644 src/integrations/stackable/interaction-types/class-interaction-type-stackable-accordion-toggle.php create mode 100644 src/integrations/stackable/interaction-types/class-interaction-type-stackable-carousel-slide-change.php create mode 100644 src/integrations/stackable/interaction-types/class-interaction-type-stackable-horizontal-scroller-scroll.php create mode 100644 src/integrations/stackable/interaction-types/class-interaction-type-stackable-tabs-change.php create mode 100644 src/integrations/stackable/interaction-types/frontend/stackableAccordionToggle.js create mode 100644 src/integrations/stackable/interaction-types/frontend/stackableCarouselSlideChange.js create mode 100644 src/integrations/stackable/interaction-types/frontend/stackableHorizontalScrollerScroll.js create mode 100644 src/integrations/stackable/interaction-types/frontend/stackableTabsChange.js diff --git a/interactions.php b/interactions.php index 6f3fcd3..98de9f9 100644 --- a/interactions.php +++ b/interactions.php @@ -126,6 +126,22 @@ function interact_require_types() { // require_once( plugin_dir_path( __FILE__ ) . 'src/interaction-types/class-interaction-type-form-submitted.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/interaction-types/class-interaction-type-element-scrolling.php' ); + // Stackable integration + if ( defined( 'STACKABLE_VERSION' ) ) { + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/action-types/class-action-type-stackable-accordion-toggle.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/interaction-types/class-interaction-type-stackable-accordion-toggle.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/action-types/class-action-type-stackable-carousel-change-slide.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/interaction-types/class-interaction-type-stackable-carousel-slide-change.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/action-types/class-action-type-stackable-tabs-change-tab.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/interaction-types/class-interaction-type-stackable-tabs-change.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/action-types/class-action-type-stackable-horizontal-scroller-scroll.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/interaction-types/class-interaction-type-stackable-horizontal-scroller-scroll.php' ); + + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/action-types/class-action-type-stackable-progress-bar-change-value.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/action-types/class-action-type-stackable-progress-circle-change-value.php' ); + require_once( plugin_dir_path( __FILE__ ) . 'src/integrations/stackable/action-types/class-action-type-stackable-count-up-reset.php' ); + } + do_action( 'interact/require_types' ); } } diff --git a/scripts/generate-frontend-php-scripts.mjs b/scripts/generate-frontend-php-scripts.mjs index fb06775..d45e14c 100644 --- a/scripts/generate-frontend-php-scripts.mjs +++ b/scripts/generate-frontend-php-scripts.mjs @@ -138,6 +138,52 @@ const processSourceDir = sourceDir => { } } ) } + + // Stackable intergration interaction types + fs.readdirSync( path.resolve( __dirname, '../src/integrations/stackable/interaction-types/frontend' ) ) + .filter( file => file.endsWith( '.js' ) ) + .forEach( async file => { + const type = file.replace( '.js', '' ) + const scriptPath = path.resolve( __dirname, `../src/integrations/stackable//interaction-types/frontend/${ file }` ) + const scriptPathRelative = path.relative( path.resolve( __dirname, '../' ), scriptPath ) + const outputFile = path.resolve( __dirname, `../dist/frontend/interactions/${ type }.php` ) + const content = fs.readFileSync( scriptPath, 'utf8' ) + + if ( buildType === 'development' ) { + writeFile( content, outputFile, scriptPathRelative ) + } else { + await minify( { + compressor: uglifyJS, + content, + output: outputFile, + } ).then( min => { + writeFile( min, outputFile, scriptPathRelative ) + } ) + } + } ) + + // Stackable intergration action types + fs.readdirSync( path.resolve( __dirname, '../src/integrations/stackable/action-types/frontend' ) ) + .filter( file => file.endsWith( '.js' ) ) + .forEach( async file => { + const type = file.replace( '.js', '' ) + const scriptPath = path.resolve( __dirname, `../src/integrations/stackable/action-types/frontend/${ file }` ) + const scriptPathRelative = path.relative( path.resolve( __dirname, '../' ), scriptPath ) + const outputFile = path.resolve( __dirname, `../dist/frontend/actions/${ type }.php` ) + const content = fs.readFileSync( scriptPath, 'utf8' ) + + if ( buildType === 'development' ) { + writeFile( content, outputFile, scriptPathRelative ) + } else { + await minify( { + compressor: uglifyJS, + content, + output: outputFile, + } ).then( min => { + writeFile( min, outputFile, scriptPathRelative ) + } ) + } + } ) } // Process all source directories diff --git a/src/action-types/frontend/backgroundColor.js b/src/action-types/frontend/backgroundColor.js index d5c4848..c5b4224 100644 --- a/src/action-types/frontend/backgroundColor.js +++ b/src/action-types/frontend/backgroundColor.js @@ -1,6 +1,26 @@ /** * This is the frontend script loaded in the frontend if the action is used. */ + +const BACKGROUND_COLOR_TARGET_SELECTORS = { + 'core/cover': [ ' .wp-block-cover__background' ], + 'core/button': [ ' .wp-element-button' ], + 'stackable/blockquote': [ ' .stk-block-blockquote__content' ], + 'stackable/button': [ ' .stk-button' ], + 'stackable/call-to-action': [ ' .stk-block-call-to-action__content' ], + 'stackable/card': [ ' .stk-block-card__content' ], + 'stackable/hero': [ ' .stk-block-hero__content' ], + 'stackable/image-box': [ ' .stk-block-image-box__content' ], + 'stackable/notification': [ ' .stk-block-notification__content' ], + 'stackable/number-box': [ ' .stk-block-number-box__container' ], + 'stackable/pricing-box': [ ' .stk-block-pricing-box__content' ], + 'stackable/testimonial': [ ' .stk-block-testimonial__content' ], + 'stackable/accordion': [ + ' .stk-block-accordion__heading', + ' .stk-block-accordion__content', + ], +} + InteractRunner.addActionConfig( { backgroundColor: { initAction: action => { @@ -9,16 +29,13 @@ InteractRunner.addActionConfig( { } ) }, // TODO: We need to move this to PHP or else we will eventually have a TON of these. - blockElementSelector: ( selector, targetBlock ) => { - // For the cover block, the target is the background element. - if ( targetBlock.isBlock( 'core/cover' ) ) { - return `${ selector } > .wp-block-cover__background` - } else if ( targetBlock.isBlock( 'core/button' ) ) { - return `${ selector } > .wp-element-button` - } else if ( targetBlock.isBlock( 'stackable/button' ) ) { - return `${ selector } > .stk-button` + blockElementSelectors: ( selector, targetBlock ) => { + for ( const [ blockName, targetSuffixes ] of Object.entries( BACKGROUND_COLOR_TARGET_SELECTORS ) ) { + if ( targetBlock.isBlock( blockName ) ) { + return targetSuffixes.map( suffix => `${ selector }${ suffix }` ) + } } - return selector + return [ selector ] }, initialStyles: action => { return `background-color: ${ action.getValue( 'color' ) };` diff --git a/src/action-types/frontend/textColor.js b/src/action-types/frontend/textColor.js index 9f6e2ea..3c60139 100644 --- a/src/action-types/frontend/textColor.js +++ b/src/action-types/frontend/textColor.js @@ -1,6 +1,25 @@ /** * This is the frontend script loaded in the frontend if the action is used. */ + +const TEXT_COLOR_TARGET_SELECTORS = { + 'core/button': [ ' .wp-element-button' ], + 'stackable/blockquote': [ ' .stk-block-blockquote__content' ], + 'stackable/button': [ ' .stk-button__inner-text' ], + 'stackable/call-to-action': [ ' .stk-block-call-to-action__content' ], + 'stackable/card': [ ' .stk-block-card__content' ], + 'stackable/hero': [ ' .stk-block-hero__content' ], + 'stackable/image-box': [ ' .stk-block-image-box__content' ], + 'stackable/notification': [ ' .stk-block-notification__content' ], + 'stackable/number-box': [ ' .stk-block-number-box__container' ], + 'stackable/pricing-box': [ ' .stk-block-pricing-box__content' ], + 'stackable/testimonial': [ ' .stk-block-testimonial__content' ], + 'stackable/accordion': [ + ' .stk-block-accordion__heading > .stk-block-column__content', + ' .stk-block-accordion__content', + ], +} + InteractRunner.addActionConfig( { textColor: { initAction: action => { @@ -9,13 +28,13 @@ InteractRunner.addActionConfig( { } ) }, // TODO: We need to move this to PHP or else we will eventually have a TON of these. - blockElementSelector: ( selector, targetBlock ) => { - if ( targetBlock.isBlock( 'core/button' ) ) { - return `${ selector } > .wp-element-button` - } else if ( targetBlock.isBlock( 'stackable/button' ) ) { - return `${ selector } .stk-button__inner-text` + blockElementSelectors: ( selector, targetBlock ) => { + for ( const [ blockName, targetSuffixes ] of Object.entries( TEXT_COLOR_TARGET_SELECTORS ) ) { + if ( targetBlock.isBlock( blockName ) ) { + return targetSuffixes.map( suffix => `${ selector }${ suffix }` ) + } } - return selector + return [ selector ] }, initialStyles: action => { return `color: ${ action.getValue( 'color' ) };` diff --git a/src/editor/editor.php b/src/editor/editor.php index a964602..97e019b 100644 --- a/src/editor/editor.php +++ b/src/editor/editor.php @@ -123,6 +123,7 @@ public function get_interaction_types_config() { 'entrance' => __( 'Entrance', 'interactions' ), 'keyboard' => __( 'Keyboard', 'interactions' ), 'misc' => __( 'Miscellaneous', 'interactions' ), + 'stackable' => __( 'Stackable', 'interactions' ), ]; $page_categories = [ @@ -220,6 +221,7 @@ public function get_action_types_config() { 'flow' => __( 'Logic Flow', 'interactions' ), 'pageState' => __( 'Page State', 'interactions' ), 'misc' => __( 'Miscellaneous', 'interactions' ), + 'stackable' => __( 'Stackable', 'interactions' ), ]; $action_categories = []; diff --git a/src/frontend/scripts/class-interaction.js b/src/frontend/scripts/class-interaction.js index 83db764..7785b89 100644 --- a/src/frontend/scripts/class-interaction.js +++ b/src/frontend/scripts/class-interaction.js @@ -146,22 +146,26 @@ export class Interaction { let actionTargetSelector = this.getTargetsSelector( action.target, triggerSelector ) // TODO: Move this to the action class - // Run the blockElementSelector function if it exists. This function + // Run the blockElementSelectors function if it exists. This function // allows us to target a specific element depending on the block. - if ( actionConfig.blockElementSelector && ( action.target.type === 'block' || action.target.type === 'block-name' || action.target.type === 'class' ) ) { + if ( actionConfig.blockElementSelectors && ( action.target.type === 'block' || action.target.type === 'block-name' || action.target.type === 'class' ) ) { const blockName = action.target.type === 'block' ? action.target.blockName : action.target.value - actionTargetSelector = actionConfig.blockElementSelector( actionTargetSelector, new TargetBlock( blockName ) ) + actionTargetSelector = actionConfig.blockElementSelectors( actionTargetSelector, new TargetBlock( blockName ) ) } - // If we're in the editor, don't set the initial styles - // unless the action is clicked. - if ( actionConfig.initialStyles && this.runner.isFrontend ) { + const actionTargetSelectors = Array.isArray( actionTargetSelector ) + ? actionTargetSelector + : [ actionTargetSelector ] + + if ( actionConfig.initialStyles ) { const styles = actionConfig.initialStyles( action ) - if ( ! cssObject[ actionTargetSelector ] ) { - cssObject[ actionTargetSelector ] = [] - } - if ( styles ) { - cssObject[ actionTargetSelector ].push( styles ) + for ( const selector of actionTargetSelectors ) { + if ( ! cssObject[ selector ] ) { + cssObject[ selector ] = [] + } + if ( styles ) { + cssObject[ selector ].push( styles ) + } } } } ) @@ -459,7 +463,7 @@ export class Interaction { return `.wp-block-${ blockName.replace( '/', '-' ) }` } - // Run the blockElementSelector function if it exists. Allow each action to + // Run the blockElementSelectors function if it exists. Allow each action to // target a specific element depending on the block if needed. For example, // for the cover block, the background color would need to be an inside // element. @@ -468,17 +472,24 @@ export class Interaction { if ( ! actionConfig ) { return targets } - return targets - .map( el => { - if ( actionConfig.blockElementSelector ) { - const selector = actionConfig.blockElementSelector( ':scope', new TargetBlock( el ) ) - if ( selector !== ':scope' ) { - return el.querySelector( selector ) || el // Make sure this is never null or the editor will error. + + return targets.flatMap( el => { + if ( actionConfig.blockElementSelectors ) { + let selectors = actionConfig.blockElementSelectors( ':scope', new TargetBlock( el ) ) + selectors = Array.isArray( selectors ) ? selectors : [ selectors ] + + const elements = selectors.flatMap( selector => { + if ( selector === ':scope' ) { + return [ el ] } - } - return el - } ) - .filter( el => el !== null && el !== undefined ) // Ensure only valid elements are included + return Array.from( el.querySelectorAll( selector ) ) + } ) + + return elements.length ? elements : [ el ] + } + + return [ el ] + } ) } } diff --git a/src/integrations/stackable/action-types/class-action-type-stackable-accordion-toggle.php b/src/integrations/stackable/action-types/class-action-type-stackable-accordion-toggle.php new file mode 100644 index 0000000..53bf26e --- /dev/null +++ b/src/integrations/stackable/action-types/class-action-type-stackable-accordion-toggle.php @@ -0,0 +1,40 @@ +name = 'stackableAccordionToggle'; + $this->category = 'stackable'; + $this->type = 'time'; + + $this->label = __( 'Stackable Accordion Toggle', 'interactions' ); + $this->description = __( 'Toggle a Stackable Accordion', 'interactions' ); + + $this->keywords = []; + + $this->properties = [ + 'stateAction' => [ + 'name' => __( 'What action to apply', 'interactions' ), + 'type' => 'select', + 'default' => 'toggle', + 'options' => [ + [ 'label' => __( 'Toggle (both Open & Close)', 'interactions' ), 'value' => 'toggle' ], + [ 'label' => __( 'Open', 'interactions' ), 'value' => 'open' ], + [ 'label' => __( 'Close', 'interactions' ), 'value' => 'close' ], + ], + ], + ]; + + $this->has_starting_state = false; + $this->has_preview = false; + $this->has_duration = false; + $this->has_easing = false; + } + } + + interact_add_action_type( 'stackableAccordionToggle', 'Interact_Action_Type_Stackable_Accordion_Toggle' ); +} diff --git a/src/integrations/stackable/action-types/class-action-type-stackable-carousel-change-slide.php b/src/integrations/stackable/action-types/class-action-type-stackable-carousel-change-slide.php new file mode 100644 index 0000000..7a7d004 --- /dev/null +++ b/src/integrations/stackable/action-types/class-action-type-stackable-carousel-change-slide.php @@ -0,0 +1,39 @@ +name = 'stackableCarouselChangeSlide'; + $this->category = 'stackable'; + $this->type = 'time'; + + $this->label = __( 'Stackable Carousel Change Slide', 'interactions' ); + $this->description = __( 'Change the current slide of the Stackable Carousel', 'interactions' ); + + $this->keywords = []; + + $this->properties = [ + 'slide' => [ + 'name' => __( 'Slide', 'interactions' ), + 'type' => 'number', + 'default' => '', + 'min' => 1, + 'max' => 10, + 'step' => 1, + 'help' => __( 'The slide number to change into. Leave this blank to change into the next slide.', 'interactions' ), + ], + ]; + + $this->has_starting_state = false; + $this->has_preview = false; + $this->has_duration = false; + $this->has_easing = false; + } + } + + interact_add_action_type( 'stackableCarouselChangeSlide', 'Interact_Action_Type_Stackable_Carousel_Change_Slide' ); +} diff --git a/src/integrations/stackable/action-types/class-action-type-stackable-count-up-reset.php b/src/integrations/stackable/action-types/class-action-type-stackable-count-up-reset.php new file mode 100644 index 0000000..195b537 --- /dev/null +++ b/src/integrations/stackable/action-types/class-action-type-stackable-count-up-reset.php @@ -0,0 +1,27 @@ +name = 'stackableCountUpReset'; + $this->category = 'stackable'; + $this->type = 'time'; + + $this->label = __( 'Stackable Count Up Reset', 'interactions' ); + $this->description = __( 'Reset the Stackable Count Up', 'interactions' ); + + $this->keywords = []; + + $this->has_starting_state = false; + $this->has_preview = false; + $this->has_duration = false; + $this->has_easing = false; + } + } + + interact_add_action_type( 'stackableCountUpReset', 'Interact_Action_Type_Stackable_Count_Up_Reset' ); +} diff --git a/src/integrations/stackable/action-types/class-action-type-stackable-horizontal-scroller-scroll.php b/src/integrations/stackable/action-types/class-action-type-stackable-horizontal-scroller-scroll.php new file mode 100644 index 0000000..f80d291 --- /dev/null +++ b/src/integrations/stackable/action-types/class-action-type-stackable-horizontal-scroller-scroll.php @@ -0,0 +1,39 @@ +name = 'stackableHorizontalScrollerScroll'; + $this->category = 'stackable'; + $this->type = 'time'; + + $this->label = __( 'Stackable Horizontal Scroll', 'interactions' ); + $this->description = __( 'Scroll to the given column number', 'interactions' ); + + $this->keywords = []; + + $this->properties = [ + 'column_number' => [ + 'name' => __( 'Column Number', 'interactions' ), + 'type' => 'number', + 'default' => 1, + 'min' => 1, + 'max' => 10, + 'step' => 1, + 'help' => __( 'The column number to change into.', 'interactions' ), + ], + ]; + + $this->has_starting_state = false; + $this->has_preview = false; + $this->has_duration = false; + $this->has_easing = false; + } + } + + interact_add_action_type( 'stackableHorizontalScrollerScroll', 'Interact_Action_Type_Stackable_Horizontal_Scroller_Scroll' ); +} diff --git a/src/integrations/stackable/action-types/class-action-type-stackable-progress-bar-change-value.php b/src/integrations/stackable/action-types/class-action-type-stackable-progress-bar-change-value.php new file mode 100644 index 0000000..61a60a8 --- /dev/null +++ b/src/integrations/stackable/action-types/class-action-type-stackable-progress-bar-change-value.php @@ -0,0 +1,39 @@ +name = 'stackableProgressBarChangeValue'; + $this->category = 'stackable'; + $this->type = 'time'; + + $this->label = __( 'Stackable Progress Bar Change Value', 'interactions' ); + $this->description = __( 'Change the value of a Stackable Progress Bar', 'interactions' ); + + $this->keywords = []; + + $this->properties = [ + 'value' => [ + 'name' => __( 'Value', 'interactions' ), + 'type' => 'number', + 'default' => 100, + 'min' => 0, + 'max' => 100, + 'step' => 0.1, + 'help' => __( 'The value to change into.', 'interactions' ), + ], + ]; + + $this->has_starting_state = false; + $this->has_preview = false; + $this->has_duration = false; + $this->has_easing = false; + } + } + + interact_add_action_type( 'stackableProgressBarChangeValue', 'Interact_Action_Type_Stackable_Progress_Bar_Change_Value' ); +} diff --git a/src/integrations/stackable/action-types/class-action-type-stackable-progress-circle-change-value.php b/src/integrations/stackable/action-types/class-action-type-stackable-progress-circle-change-value.php new file mode 100644 index 0000000..622d2a8 --- /dev/null +++ b/src/integrations/stackable/action-types/class-action-type-stackable-progress-circle-change-value.php @@ -0,0 +1,39 @@ +name = 'stackableProgressCircleChangeValue'; + $this->category = 'stackable'; + $this->type = 'time'; + + $this->label = __( 'Stackable Progress Circle Change Value', 'interactions' ); + $this->description = __( 'Change the value of a Stackable Progress Circle', 'interactions' ); + + $this->keywords = []; + + $this->properties = [ + 'value' => [ + 'name' => __( 'Value', 'interactions' ), + 'type' => 'number', + 'default' => 100, + 'min' => 0, + 'max' => 100, + 'step' => 1, + 'help' => __( 'The value to change into.', 'interactions' ), + ], + ]; + + $this->has_starting_state = false; + $this->has_preview = false; + $this->has_duration = false; + $this->has_easing = false; + } + } + + interact_add_action_type( 'stackableProgressCircleChangeValue', 'Interact_Action_Type_Stackable_Progress_Circle_Change_Value' ); +} diff --git a/src/integrations/stackable/action-types/class-action-type-stackable-tabs-change-tab.php b/src/integrations/stackable/action-types/class-action-type-stackable-tabs-change-tab.php new file mode 100644 index 0000000..6bb148b --- /dev/null +++ b/src/integrations/stackable/action-types/class-action-type-stackable-tabs-change-tab.php @@ -0,0 +1,39 @@ +name = 'stackableTabsChangeTab'; + $this->category = 'stackable'; + $this->type = 'time'; + + $this->label = __( 'Stackable Change Tab', 'interactions' ); + $this->description = __( 'Change the current tab of the Stackable Tabs', 'interactions' ); + + $this->keywords = []; + + $this->properties = [ + 'tab' => [ + 'name' => __( 'Tab', 'interactions' ), + 'type' => 'number', + 'default' => 1, + 'min' => 1, + 'max' => 10, + 'step' => 1, + 'help' => __( 'The tab number to change into.', 'interactions' ), + ], + ]; + + $this->has_starting_state = false; + $this->has_preview = false; + $this->has_duration = false; + $this->has_easing = false; + } + } + + interact_add_action_type( 'stackableTabsChangeTab', 'Interact_Action_Type_Stackable_Tabs_Change_Tab' ); +} diff --git a/src/integrations/stackable/action-types/frontend/stackableAccordionToggle.js b/src/integrations/stackable/action-types/frontend/stackableAccordionToggle.js new file mode 100644 index 0000000..309131c --- /dev/null +++ b/src/integrations/stackable/action-types/frontend/stackableAccordionToggle.js @@ -0,0 +1,32 @@ +/** + * This is the frontend script loaded in the frontend if the action is used. + */ + +InteractRunner.addActionConfig( { + stackableAccordionToggle: { + initAction: action => { + action.initActionFunction( () => { + const stateAction = action.getValue( 'stateAction' ) || 'toggle' + + action.getTargets().forEach( el => { + // Do not proceed if the target is not a Stackable Accordion + if ( ! el.classList?.contains( 'wp-block-stackable-accordion' ) ) { + return + } + + switch ( stateAction ) { + case 'toggle': + el.open = ! el.open + break + case 'open': + el.open = true + break + case 'close': + el.open = false + break + } + } ) + } ) + }, + }, +} ) diff --git a/src/integrations/stackable/action-types/frontend/stackableCarouselChangeSlide.js b/src/integrations/stackable/action-types/frontend/stackableCarouselChangeSlide.js new file mode 100644 index 0000000..cb0cd5a --- /dev/null +++ b/src/integrations/stackable/action-types/frontend/stackableCarouselChangeSlide.js @@ -0,0 +1,28 @@ +/** + * This is the frontend script loaded in the frontend if the action is used. + */ + +InteractRunner.addActionConfig( { + stackableCarouselChangeSlide: { + initAction: action => { + action.initActionFunction( () => { + const slide = action.getValue( 'slide' ) || '' + + action.getTargets().forEach( el => { + const carousel = el.carousel + + // Do not proceed if the target is not a Stackable Carousel + if ( ! carousel ) { + return + } + + if ( slide === '' ) { + carousel.nextSlide() + } else { + carousel.goToSlide( Number( slide ) ) + } + } ) + } ) + }, + }, +} ) diff --git a/src/integrations/stackable/action-types/frontend/stackableCountUpReset.js b/src/integrations/stackable/action-types/frontend/stackableCountUpReset.js new file mode 100644 index 0000000..341e62f --- /dev/null +++ b/src/integrations/stackable/action-types/frontend/stackableCountUpReset.js @@ -0,0 +1,24 @@ +/** + * This is the frontend script loaded in the frontend if the action is used. + */ + +InteractRunner.addActionConfig( { + stackableCountUpReset: { + initAction: action => { + action.initActionFunction( () => { + action.getTargets().forEach( el => { + const text = el.querySelector( '.stk-block-count-up__text' ) + const countUp = text.countUp + + // Do not proceed if the target is not a Stackable Count Up + if ( ! countUp ) { + return + } + + // Re-initialize the count up block + countUp.init() + } ) + } ) + }, + }, +} ) diff --git a/src/integrations/stackable/action-types/frontend/stackableHorizontalScrollerScroll.js b/src/integrations/stackable/action-types/frontend/stackableHorizontalScrollerScroll.js new file mode 100644 index 0000000..a5daf58 --- /dev/null +++ b/src/integrations/stackable/action-types/frontend/stackableHorizontalScrollerScroll.js @@ -0,0 +1,40 @@ +/** + * This is the frontend script loaded in the frontend if the action is used. + */ + +InteractRunner.addActionConfig( { + stackableHorizontalScrollerScroll: { + initAction: action => { + action.initActionFunction( () => { + const columnNumber = action.getValue( 'column_number' ) || 1 + + action.getTargets().forEach( el => { + // Do not proceed if the target is not a horizontal scroller + if ( ! el.classList?.contains( 'wp-block-stackable-horizontal-scroller' ) ) { + return + } + + // Horizontal scroller operations are done to the inner block content + const blockContent = el.querySelector( '.stk-block-content' ) + const child = blockContent?.children?.[ 0 ] + const columnWidth = child.clienWidth ?? 300 + const scrollSnapAlign = window.getComputedStyle( child )?.scrollSnapAlign ?? 'none' + + let scrollAmount = 0 + if ( scrollSnapAlign === 'start' || scrollSnapAlign === 'none' ) { + scrollAmount = columnWidth * ( columnNumber - 1 ) + } else if ( scrollSnapAlign === 'center' ) { + scrollAmount = columnWidth * ( columnNumber - 1.5 ) + } else if ( scrollSnapAlign === 'end' ) { + scrollAmount = columnWidth * ( columnNumber - 2 ) + } + + blockContent.scrollTo( { + left: scrollAmount, + behavior: 'smooth', + } ) + } ) + } ) + }, + }, +} ) diff --git a/src/integrations/stackable/action-types/frontend/stackableProgressBarChangeValue.js b/src/integrations/stackable/action-types/frontend/stackableProgressBarChangeValue.js new file mode 100644 index 0000000..1a35ffe --- /dev/null +++ b/src/integrations/stackable/action-types/frontend/stackableProgressBarChangeValue.js @@ -0,0 +1,26 @@ +/** + * This is the frontend script loaded in the frontend if the action is used. + */ + +InteractRunner.addActionConfig( { + stackableProgressBarChangeValue: { + initAction: action => { + action.initActionFunction( () => { + const value = String( action.getValue( 'value' ) || 100 ) + + action.getTargets().forEach( el => { + // Do not proceed if the target is not a horizontal scroller + if ( ! el.classList?.contains( 'wp-block-stackable-progress-bar' ) ) { + return + } + + const bar = el.querySelector( '.stk-progress-bar__bar' ) + const text = el.querySelector( '.stk-progress-bar__progress-value-text' ) + + bar.style.width = `${ value }%` + text.textContent = value + } ) + } ) + }, + }, +} ) diff --git a/src/integrations/stackable/action-types/frontend/stackableProgressCircleChangeValue.js b/src/integrations/stackable/action-types/frontend/stackableProgressCircleChangeValue.js new file mode 100644 index 0000000..57ef5b2 --- /dev/null +++ b/src/integrations/stackable/action-types/frontend/stackableProgressCircleChangeValue.js @@ -0,0 +1,26 @@ +/** + * This is the frontend script loaded in the frontend if the action is used. + */ + +InteractRunner.addActionConfig( { + stackableProgressCircleChangeValue: { + initAction: action => { + action.initActionFunction( () => { + const value = String( action.getValue( 'value' ) || 100 ) + + action.getTargets().forEach( el => { + // Do not proceed if the target is not a horizontal scroller + if ( ! el.classList?.contains( 'wp-block-stackable-progress-circle' ) ) { + return + } + + const circle = el.querySelector( '.stk-progress-circle' ) + const text = el.querySelector( '.stk-progress-circle__inner-text' ) + + circle.style.setProperty( '--progress-value', value, 'important' ) + text.textContent = value + } ) + } ) + }, + }, +} ) diff --git a/src/integrations/stackable/action-types/frontend/stackableTabsChangeTab.js b/src/integrations/stackable/action-types/frontend/stackableTabsChangeTab.js new file mode 100644 index 0000000..29fe3cc --- /dev/null +++ b/src/integrations/stackable/action-types/frontend/stackableTabsChangeTab.js @@ -0,0 +1,24 @@ +/** + * This is the frontend script loaded in the frontend if the action is used. + */ + +InteractRunner.addActionConfig( { + stackableTabsChangeTab: { + initAction: action => { + action.initActionFunction( () => { + const tabNumber = action.getValue( 'tab' ) || 1 + + action.getTargets().forEach( el => { + const tabs = el.tabs + + // Do not proceed if the target is not a Stackable Tabs + if ( ! tabs ) { + return + } + + tabs.changeTab( Number( tabNumber ) ) + } ) + } ) + }, + }, +} ) diff --git a/src/integrations/stackable/interaction-types/class-interaction-type-stackable-accordion-toggle.php b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-accordion-toggle.php new file mode 100644 index 0000000..342b00d --- /dev/null +++ b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-accordion-toggle.php @@ -0,0 +1,47 @@ +name = 'stackableAccordionToggle'; + $this->type = 'element'; + $this->category = 'stackable'; + + $this->label = __( 'Stackable Accordion Toggle', 'interactions' ); + $this->description = __( 'Define actions that happen when you toggle the accordion', 'interactions' ); + $this->timelines = [ + [ + 'title' => __( 'Toggle Actions', 'interactions' ), + 'slug' => 'accordion', + 'description' => '', + ], + ]; + $this->timeline_type = 'time'; + + $this->options = [ + [ + 'label' => __( 'When to apply actions', 'interactions' ), + 'name' => 'stateAction', + 'type' => 'select', + 'options' => [ + [ 'label' => __( 'Toggle (both Open & Close)', 'interactions' ), 'value' => 'toggle' ], + [ 'label' => __( 'Open', 'interactions' ), 'value' => 'open' ], + [ 'label' => __( 'Close', 'interactions' ), 'value' => 'close' ], + ], + 'default' => 'toggle', + 'help' => __( 'The state of the accordion when the actions are executed', 'interactions' ), + ], + ]; + } + } + + interact_add_interaction_type( 'stackableAccordionToggle', 'Interact_Interaction_Type_Stackable_Accordion_Toggle' ); +} diff --git a/src/integrations/stackable/interaction-types/class-interaction-type-stackable-carousel-slide-change.php b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-carousel-slide-change.php new file mode 100644 index 0000000..e289451 --- /dev/null +++ b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-carousel-slide-change.php @@ -0,0 +1,45 @@ +name = 'stackableCarouselSlideChange'; + $this->type = 'element'; + $this->category = 'stackable'; + + $this->label = __( 'Stackable Carousel Slide Change', 'interactions' ); + $this->description = __( 'Define actions that happen when the carousel changes its current slide', 'interactions' ); + $this->timelines = [ + [ + 'title' => __( 'Slide Change Actions', 'interactions' ), + 'slug' => 'carousel', + 'description' => '', + ], + ]; + $this->timeline_type = 'time'; + + $this->options = [ + [ + 'label' => __( 'Slide', 'interactions' ), + 'name' => 'slide', + 'type' => 'number', + 'default' => '', + 'min' => 1, + 'max' => 10, + 'step' => 1, + 'help' => __( 'When the carousel changes into this slide number, trigger the actions. Leave this blank to trigger for every slide change.', 'interactions' ), + ], + ]; + } + } + + interact_add_interaction_type( 'stackableCarouselSlideChange', 'Interact_Interaction_Type_Stackable_Carousel_Slide_Change' ); +} diff --git a/src/integrations/stackable/interaction-types/class-interaction-type-stackable-horizontal-scroller-scroll.php b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-horizontal-scroller-scroll.php new file mode 100644 index 0000000..74c3058 --- /dev/null +++ b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-horizontal-scroller-scroll.php @@ -0,0 +1,45 @@ +name = 'stackableHorizontalScrollerScroll'; + $this->type = 'element'; + $this->category = 'stackable'; + + $this->label = __( 'Stackable Horizontal Scroller Scroll', 'interactions' ); + $this->description = __( 'Define actions that happen when the horizontal scroller is scrolled', 'interactions' ); + $this->timelines = [ + [ + 'title' => __( 'Horizontal Scroller Actions', 'interactions' ), + 'slug' => 'horizontal-scroller', + 'description' => '', + ], + ]; + $this->timeline_type = 'time'; + + $this->options = [ + [ + 'label' => __( 'Column', 'interactions' ), + 'name' => 'column', + 'type' => 'number', + 'default' => '', + 'min' => 1, + 'max' => 10, + 'step' => 1, + 'help' => __( 'When the horizontal scroller changes into this column number, trigger the actions. Leave this blank to trigger when scrolled', 'interactions' ), + ], + ]; + } + } + + interact_add_interaction_type( 'stackableHorizontalScrollerScroll', 'Interact_Interaction_Type_Stackable_Horizontal_Scroller_Scroll' ); +} diff --git a/src/integrations/stackable/interaction-types/class-interaction-type-stackable-tabs-change.php b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-tabs-change.php new file mode 100644 index 0000000..d8f956d --- /dev/null +++ b/src/integrations/stackable/interaction-types/class-interaction-type-stackable-tabs-change.php @@ -0,0 +1,45 @@ +name = 'stackableTabsChange'; + $this->type = 'element'; + $this->category = 'stackable'; + + $this->label = __( 'Stackable Tabs Change', 'interactions' ); + $this->description = __( 'Define actions that happen when the tabs block changes its current tab', 'interactions' ); + $this->timelines = [ + [ + 'title' => __( 'Tabs Change Actions', 'interactions' ), + 'slug' => 'tabs', + 'description' => '', + ], + ]; + $this->timeline_type = 'time'; + + $this->options = [ + [ + 'label' => __( 'Tab', 'interactions' ), + 'name' => 'tab', + 'type' => 'number', + 'default' => '', + 'min' => 1, + 'max' => 10, + 'step' => 1, + 'help' => __( 'When the tabs block changes into this tab, trigger the actions. Leave this blank to trigger for every tab change.', 'interactions' ), + ], + ]; + } + } + + interact_add_interaction_type( 'stackableTabsChange', 'Interact_Interaction_Type_Stackable_Tabs_Change' ); +} diff --git a/src/integrations/stackable/interaction-types/frontend/stackableAccordionToggle.js b/src/integrations/stackable/interaction-types/frontend/stackableAccordionToggle.js new file mode 100644 index 0000000..5d1bede --- /dev/null +++ b/src/integrations/stackable/interaction-types/frontend/stackableAccordionToggle.js @@ -0,0 +1,36 @@ +/** + * This is the frontend script loaded in the frontend if the interaction is used. + */ +InteractRunner.addInteractionConfig( { + stackableAccordionToggle: { + initTimeline: interaction => { + const stateAction = interaction.getOption( 'stateAction', 'toggle' ) + + let timeline = null + const handler = event => { + const isOpen = event.newState ? event.newState === 'open' : el.open + + if ( stateAction === 'toggle' || + ( stateAction === 'open' && isOpen ) || + ( stateAction === 'close' && ! isOpen ) ) { + timeline?.destroy( false ) + + timeline = interaction.createTimelineInstance( 0 ) + timeline?.play() + } + } + + const el = interaction.getCurrentTrigger() + + // Do not proceed if the target is not a Stackable Accordion + if ( el.classList?.contains( 'wp-block-stackable-accordion' ) ) { + el.addEventListener( 'toggle', handler ) + + return () => { + timeline?.destroy() + el.removeEventListener( 'toggle', handler ) + } + } + }, + }, +} ) diff --git a/src/integrations/stackable/interaction-types/frontend/stackableCarouselSlideChange.js b/src/integrations/stackable/interaction-types/frontend/stackableCarouselSlideChange.js new file mode 100644 index 0000000..12e8e72 --- /dev/null +++ b/src/integrations/stackable/interaction-types/frontend/stackableCarouselSlideChange.js @@ -0,0 +1,35 @@ +/** + * This is the frontend script loaded in the frontend if the interaction is used. + */ +InteractRunner.addInteractionConfig( { + stackableCarouselSlideChange: { + initTimeline: interaction => { + const slide = interaction.getOption( 'slide', '' ) + + let timeline = null + const handler = event => { + const currentSlide = event.detail?.currentSlide + // If a slide number is provided, trigger when the carousel changes into that slide, + // otherwise, trigger on every slide change. + if ( slide === '' || Number( slide ) === currentSlide ) { + timeline?.destroy( false ) + + timeline = interaction.createTimelineInstance( 0 ) + timeline?.play() + } + } + + const el = interaction.getCurrentTrigger() + + // Do not proceed if the target is not a Stackable Carousel + if ( el.classList?.contains( 'wp-block-stackable-carousel' ) ) { + el.addEventListener( 'stackable-carousel-slide-change', handler ) + + return () => { + timeline?.destroy() + el.removeEventListener( 'stackable-carousel-slide-change', handler ) + } + } + }, + }, +} ) diff --git a/src/integrations/stackable/interaction-types/frontend/stackableHorizontalScrollerScroll.js b/src/integrations/stackable/interaction-types/frontend/stackableHorizontalScrollerScroll.js new file mode 100644 index 0000000..ff933c7 --- /dev/null +++ b/src/integrations/stackable/interaction-types/frontend/stackableHorizontalScrollerScroll.js @@ -0,0 +1,60 @@ +/** + * This is the frontend script loaded in the frontend if the interaction is used. + */ + +InteractRunner.addInteractionConfig( { + stackableHorizontalScrollerScroll: { + initTimeline: interaction => { + const column = interaction.getOption( 'column', '' ) + + let timeline = null + let isScrolling + + const handler = event => { + const target = event.target + const child = target.children[ 0 ] + const length = target.children.length + + const columnWidth = child.clientWidth ?? 300 + const scrollSnapAlign = window.getComputedStyle( child )?.scrollSnapAlign ?? 'none' + const maxScrollLeft = target.scrollWidth - target.clientWidth + + let currentColumns = [] + + if ( target.scrollLeft === maxScrollLeft ) { + const currentAmount = Math.floor( target.clientWidth / columnWidth ) + currentColumns = Array.from( { length: currentAmount }, ( _, i ) => length - currentAmount + i + 1 ) + } else if ( scrollSnapAlign === 'start' || scrollSnapAlign === 'none' ) { + currentColumns = [ Math.floor( target.scrollLeft / columnWidth ) + 1 ] + } else if ( scrollSnapAlign === 'center' ) { + currentColumns = [ Math.floor( ( target.scrollLeft + ( ( target.clientWidth - columnWidth ) / 2 ) ) / columnWidth ) + 1 ] + } + + clearTimeout( isScrolling ) + if ( column === '' || currentColumns.includes( Number( column ) ) ) { + isScrolling = setTimeout( () => { + timeline?.destroy( false ) + + timeline = interaction.createTimelineInstance( 0 ) + timeline?.play() + }, 100 ) + } + } + + const el = interaction.getCurrentTrigger() + + // Do not proceed if the target is not a Stackable Tabs + if ( el.classList?.contains( 'wp-block-stackable-horizontal-scroller' ) ) { + // Horizontal scroller operations are done to the inner block content + const blockContent = el.querySelector( '.stk-block-content' ) + + blockContent.addEventListener( 'scroll', handler ) + + return () => { + timeline?.destroy() + blockContent.removeEventListener( 'scroll', handler ) + } + } + }, + }, +} ) diff --git a/src/integrations/stackable/interaction-types/frontend/stackableTabsChange.js b/src/integrations/stackable/interaction-types/frontend/stackableTabsChange.js new file mode 100644 index 0000000..aec57b6 --- /dev/null +++ b/src/integrations/stackable/interaction-types/frontend/stackableTabsChange.js @@ -0,0 +1,35 @@ +/** + * This is the frontend script loaded in the frontend if the interaction is used. + */ +InteractRunner.addInteractionConfig( { + stackableTabsChange: { + initTimeline: interaction => { + const tab = interaction.getOption( 'tab', '' ) + + let timeline = null + const handler = event => { + const activeTab = event.detail?.activeTab + // If a tab number is provided, trigger when the tabs block changes into that tab, + // otherwise, trigger on every tab change. + if ( tab === '' || Number( tab ) === activeTab ) { + timeline?.destroy( false ) + + timeline = interaction.createTimelineInstance( 0 ) + timeline?.play() + } + } + + const el = interaction.getCurrentTrigger() + + // Do not proceed if the target is not a Stackable Tabs + if ( el.classList?.contains( 'wp-block-stackable-tabs' ) ) { + el.addEventListener( 'stackable-tabs-change', handler ) + + return () => { + timeline?.destroy() + el.removeEventListener( 'stackable-tabs-change', handler ) + } + } + }, + }, +} )