Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions assets/config/animations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"defaults": {
"duration": 1,
"ease": "power2.out",
"distance": 24,
"stagger": 0.2
},
"css": {
"fade": "opacity: 0;",
"fade-up": "opacity: 0;",
"fade-down": "opacity: 0;",
"fade-left": "opacity: 0;",
"fade-right": "opacity: 0;",
"zoom-in": "opacity: 0; transform: scale(0.8);"
},
"types": {
"fade": {
"mappings": {
"[class*=\"is-style-ext-preset--image--natural-1--image-\"]": "fade",
"[class*=\"is-style-ext-preset--group--natural-1--item-card-1--\"]": "fade",
"[class*=\"is-style-ext-preset--button--natural-1--button-\"]": "fade",
"[class*=\"is-style-ext-preset--group--natural-1--header-1\"]": "fade",
"[class*=\"is-style-ext-preset--group--natural-1--footer-1\"]": "fade",
"[class*=\"is-style-ext-preset--media-text--natural-1\"]": "fade",
"[class*=\"is-style-ext-preset--cover--natural-1--cover-overlay-1\"]": "fade"

}
},
"fade-up": {
"mappings": {
"[class*=\"is-style-ext-preset--image--natural-1--image-\"]": "fade-up",
"[class*=\"is-style-ext-preset--group--natural-1--item-card-1--\"]": "fade-up",
"[class*=\"is-style-ext-preset--button--natural-1--button-\"]": "fade-up",
"[class*=\"is-style-ext-preset--group--natural-1--header-1\"]": "fade-up",
"[class*=\"is-style-ext-preset--group--natural-1--footer-1\"]": "fade-up",
"[class*=\"is-style-ext-preset--media-text--natural-1\"]": "fade-up",
"[class*=\"is-style-ext-preset--cover--natural-1--cover-overlay-1\"]": "fade-up"
}
},
"zoom-in": {
"mappings": {
"[class*=\"is-style-ext-preset--image--natural-1--image-\"]": "zoom-in",
"[class*=\"is-style-ext-preset--group--natural-1--item-card-1--\"]": "zoom-in",
"[class*=\"is-style-ext-preset--button--natural-1--button-\"]": "zoom-in",
"[class*=\"is-style-ext-preset--group--natural-1--header-1\"]": "fade",
"[class*=\"is-style-ext-preset--group--natural-1--footer-1\"]": "fade",
"[class*=\"is-style-ext-preset--media-text--natural-1\"]": "zoom-in",
"[class*=\"is-style-ext-preset--cover--natural-1--cover-overlay-1\"]": "fade"
}
}
}
}
164 changes: 164 additions & 0 deletions assets/js/animations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
(function () {
'use strict';

function normalizeSelector(selector) {
if (!selector || typeof selector !== 'string') return null;
const trimmed = selector.trim();
if (!trimmed) return null;

// If the DB stores plain class names like "is-style-fade-up",
// treat them as classes. If user stores ".foo" or "#bar", keep as-is.
if (trimmed.startsWith('.') || trimmed.startsWith('#') || trimmed.startsWith('[')) {
return trimmed;
}

return `.${trimmed}`;
}

function getConfig() {
const cfg = window.ExtendableAnimations || {};
const defaults = cfg.defaults || {};

return {
map: cfg.map && typeof cfg.map === 'object' ? cfg.map : {},
defaults: {
duration: Number.isFinite(defaults.duration) ? defaults.duration : 0.7,
ease: typeof defaults.ease === 'string' ? defaults.ease : 'power2.out',
distance: Number.isFinite(defaults.distance) ? defaults.distance : 24,
},
};
}

function buildAnimationProps(animationKey, defaults) {
const distance = defaults.distance;
const delay = defaults.delay || 0;

// Base fade-in
const base = {
from: { opacity: 0 },
to: { opacity: 1, duration: defaults.duration, ease: defaults.ease, delay: delay },
};

switch ((animationKey || '').toLowerCase()) {
case 'fade-up':
return {
from: { opacity: 0, y: distance },
to: { opacity: 1, y: 0, duration: defaults.duration, ease: defaults.ease, delay: delay },
};
case 'fade-down':
return {
from: { opacity: 0, y: -distance },
to: { opacity: 1, y: 0, duration: defaults.duration, ease: defaults.ease, delay: delay },
};
case 'fade-left':
return {
from: { opacity: 0, x: distance },
to: { opacity: 1, x: 0, duration: defaults.duration, ease: defaults.ease, delay: delay },
};
case 'fade-right':
return {
from: { opacity: 0, x: -distance },
to: { opacity: 1, x: 0, duration: defaults.duration, ease: defaults.ease, delay: delay },
};
case 'zoom-in':
return {
from: { opacity: 0, scale: 0.8 },
to: { opacity: 1, scale: 1, duration: defaults.duration, ease: defaults.ease, delay: delay },
};
case 'fade':
default:
return base;
}
}

function animateOnScroll(element, animationKey, defaults) {
if (!element || element.nodeType !== 1) return;
if (element.dataset && element.dataset.extendableAnimated === '1') return;

const gsap = window.gsap;
if (!gsap) return;

const props = buildAnimationProps(animationKey, defaults);

// Set initial state once.
gsap.set(element, props.from);

const ScrollTrigger = window.ScrollTrigger;
if (ScrollTrigger && typeof gsap.registerPlugin === 'function') {
gsap.registerPlugin(ScrollTrigger);

gsap.to(element, {
...props.to,
scrollTrigger: {
trigger: element,
start: 'top 85%',
once: true,
},
});

element.dataset.extendableAnimated = '1';
return;
}

// Fallback without ScrollTrigger: IntersectionObserver.
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
gsap.to(element, props.to);
element.dataset.extendableAnimated = '1';
io.disconnect();
});
},
{ root: null, threshold: 0.15 }
);

io.observe(element);
return;
}

// Last resort: animate immediately.
gsap.to(element, props.to);
element.dataset.extendableAnimated = '1';
}

function run() {
const { map, defaults } = getConfig();

// Nothing configured: do nothing.
const keys = Object.keys(map);
if (!keys.length) return;

const gsap = window.gsap;
if (!gsap) return;

keys.forEach((selectorOrClass) => {
const selector = normalizeSelector(selectorOrClass);
if (!selector) return;

const animationKey = map[selectorOrClass];
const elements = document.querySelectorAll(selector);
if (!elements || !elements.length) return;

// Use stagger if multiple elements
if (elements.length > 1) {
const staggerDelay = defaults.stagger || 0.2;
elements.forEach((el, index) => {
if (!el.dataset.extendableAnimated) {
const customDefaults = { ...defaults, delay: index * staggerDelay };
animateOnScroll(el, animationKey, customDefaults);
}
});
} else {
elements.forEach((el) => animateOnScroll(el, animationKey, defaults));
}
});
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
})();
4 changes: 4 additions & 0 deletions bin/build-zip.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ assets=$(ls assets/css/*.css)
fonts=$(ls assets/fonts/**/*.{woff2,txt})
tribeEvents=$(ls tribe-events/*.css)
assetsJs=$(ls assets/js/*.js)
vendorJs=$(find assets/vendor -type f -name "*.js" 2>/dev/null || true)
configJson=$(find assets/config -type f -name "*.json" 2>/dev/null || true)

zip -r ./build/extendable.zip \
$toplevelFiles \
Expand All @@ -48,6 +50,8 @@ zip -r ./build/extendable.zip \
$fonts \
$tribeEvents \
$assetsJs \
$vendorJs \
$configJson \

unzip ./build/extendable.zip -d "./build/extendable/"

Expand Down
111 changes: 111 additions & 0 deletions functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,117 @@ function extendable_enqueue_navigation_customizations() {
endif;
add_action( 'wp_enqueue_scripts', 'extendable_enqueue_navigation_customizations' );

/**
* Animations
*
* @since Extendable 2.0.34
* @return void
*/
function extendable_enqueue_animations() {

$animation_option = get_option( 'extendable_animation_type', "fade" );

if ( is_admin() ||
! apply_filters( 'extendable_enable_animations', true ) ||
false === $animation_option ||
empty( $animation_option ) ) {
return;
}

$config_file = get_template_directory() . '/assets/config/animations.json';

if ( ! file_exists( $config_file ) || ! is_readable( $config_file ) ) {
return;
}

$config = json_decode( file_get_contents( $config_file ), true );

if ( ! is_array( $config ) ) {
return;
}

$type = sanitize_key( $animation_option );

if ( ! isset( $config['types'][ $type ] ) ) {
$type = 'fade';
}

$mappings = $config['types'][ $type ]['mappings'] ?? array();
$defaults = $config['defaults'] ?? array();
$css_config = $config['css'] ?? array();

$sanitized = array();
foreach ( $mappings as $selector => $animation ) {
$clean_selector = sanitize_text_field( trim( $selector ) );
$clean_animation = sanitize_key( trim( $animation ) );

if ( ! empty( $clean_selector ) && ! empty( $clean_animation ) ) {
$sanitized[ $clean_selector ] = $clean_animation;
}
}

if ( empty( $sanitized ) ) {
return;
}

$gsap_path = get_template_directory() . '/assets/vendor/gsap/';
$gsap_uri = get_template_directory_uri() . '/assets/vendor/gsap/';

$gsap_scripts = array(
'extendable-gsap' => array(
'file' => 'gsap.min.js',
'deps' => array()
),
'extendable-gsap-scrolltrigger' => array(
'file' => 'ScrollTrigger.min.js',
'deps' => array( 'extendable-gsap' )
)
);

foreach ( $gsap_scripts as $handle => $script_config ) {
$file_path = $gsap_path . $script_config['file'];

if ( file_exists( $file_path ) ) {
wp_enqueue_script(
$handle,
$gsap_uri . $script_config['file'],
$script_config['deps'],
filemtime( $file_path ),
true
);
}
}

wp_enqueue_script(
'extendable-animations',
get_template_directory_uri() . '/assets/js/animations.js',
array( 'extendable-gsap-scrolltrigger' ),
EXTENDABLE_THEME_VERSION,
true
);

wp_localize_script( 'extendable-animations', 'ExtendableAnimations', array(
'map' => $sanitized,
'defaults' => array_map( 'sanitize_text_field', $defaults ),
));

// Generate FOUC prevention CSS
$animation_css = '';
foreach ( $sanitized as $selector => $animation ) {
$css_rule = isset( $css_config[ $animation ] ) && ! empty( $css_config[ $animation ] )
? sanitize_text_field( $css_config[ $animation ] )
: 'opacity: 0;';

$animation_css .= sprintf( '%s { %s } ', esc_attr( $selector ), $css_rule );
}

if ( ! empty( $animation_css ) ) {
wp_add_inline_style( 'extendable-style', $animation_css );
}
}
add_action( 'wp_enqueue_scripts', 'extendable_enqueue_animations' );


/**
* Set default template for new pages in the block editor (auto-drafts)
*
Expand Down