This commit is contained in:
Simon Huber 2025-12-02 14:33:57 +01:00 committed by GitHub
commit bc840fa471
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 912 additions and 69 deletions

View file

@ -9,34 +9,539 @@ A simple slide animation that varies the height of the element
"use strict";
// Helper function to get current computed dimensions during animation
function getCurrentDimensions(domNode, isHorizontal) {
var computedStyle = window.getComputedStyle(domNode);
if(isHorizontal) {
return {
size: parseFloat(computedStyle.width) || 0,
marginStart: parseFloat(computedStyle.marginLeft) || 0,
marginEnd: parseFloat(computedStyle.marginRight) || 0,
paddingStart: parseFloat(computedStyle.paddingLeft) || 0,
paddingEnd: parseFloat(computedStyle.paddingRight) || 0,
opacity: parseFloat(computedStyle.opacity) || 0
};
} else {
return {
size: parseFloat(computedStyle.height) || 0,
marginStart: parseFloat(computedStyle.marginTop) || 0,
marginEnd: parseFloat(computedStyle.marginBottom) || 0,
paddingStart: parseFloat(computedStyle.paddingTop) || 0,
paddingEnd: parseFloat(computedStyle.paddingBottom) || 0,
opacity: parseFloat(computedStyle.opacity) || 0
};
}
}
// Helper to check if element is currently animating
function isAnimating(domNode) {
return domNode.dataset.slideAnimating === "true";
}
// Helper to mark animation state
function setAnimating(domNode, state) {
if(state) {
domNode.dataset.slideAnimating = "true";
} else {
delete domNode.dataset.slideAnimating;
}
}
function slideOpen(domNode,options) {
options = options || {};
var duration = options.duration || $tw.utils.getAnimationDuration();
// Get the current height of the domNode
var direction = options.direction || "vertical";
var isHorizontal = direction === "horizontal";
var origin = options.origin || (isHorizontal ? "left" : "top");
// If currently animating, capture current state
var startState;
var needsTargetDimensions = true;
if(isAnimating(domNode)) {
startState = getCurrentDimensions(domNode, isHorizontal);
// Remove any existing transition end handlers
var oldHandler = domNode._slideTransitionHandler;
if(oldHandler) {
domNode.removeEventListener("transitionend", oldHandler);
}
// Always recalculate target dimensions when opening
// (the element's natural size may have changed while it was closing)
needsTargetDimensions = true;
} else {
startState = {
size: 0,
marginStart: 0,
marginEnd: 0,
paddingStart: 0,
paddingEnd: 0,
opacity: 0
};
}
// Get or update target dimensions
if(needsTargetDimensions) {
// If we're in the middle of an animation, we need to temporarily reset the element
// to get its natural dimensions
var originalStyles = {};
if(isAnimating(domNode)) {
// Store current styles
originalStyles.height = domNode.style.height;
originalStyles.width = domNode.style.width;
originalStyles.transition = domNode.style.transition;
// Temporarily reset to get natural dimensions
domNode.style.transition = "none";
domNode.style.height = "";
domNode.style.width = "";
$tw.utils.forceLayout(domNode);
}
var computedStyle = window.getComputedStyle(domNode),
currMarginBottom = parseInt(computedStyle.marginBottom,10),
currMarginTop = parseInt(computedStyle.marginTop,10),
currPaddingBottom = parseInt(computedStyle.paddingBottom,10),
currPaddingTop = parseInt(computedStyle.paddingTop,10),
currHeight = domNode.offsetHeight;
// Reset the margin once the transition is over
setTimeout(function() {
targetMarginBottom = parseInt(computedStyle.marginBottom,10),
targetMarginTop = parseInt(computedStyle.marginTop,10),
targetMarginLeft = parseInt(computedStyle.marginLeft,10),
targetMarginRight = parseInt(computedStyle.marginRight,10),
targetPaddingBottom = parseInt(computedStyle.paddingBottom,10),
targetPaddingTop = parseInt(computedStyle.paddingTop,10),
targetPaddingLeft = parseInt(computedStyle.paddingLeft,10),
targetPaddingRight = parseInt(computedStyle.paddingRight,10),
targetHeight = domNode.offsetHeight,
targetWidth = domNode.offsetWidth;
// Restore original styles if we changed them
if(isAnimating(domNode)) {
domNode.style.height = originalStyles.height;
domNode.style.width = originalStyles.width;
domNode.style.transition = originalStyles.transition;
}
// Store target dimensions for potential interruption
domNode._slideTargetDimensions = {
marginBottom: targetMarginBottom,
marginTop: targetMarginTop,
marginLeft: targetMarginLeft,
marginRight: targetMarginRight,
paddingBottom: targetPaddingBottom,
paddingTop: targetPaddingTop,
paddingLeft: targetPaddingLeft,
paddingRight: targetPaddingRight,
height: targetHeight,
width: targetWidth
};
}
// Mark as animating
setAnimating(domNode, true);
// Reset the properties once the transition is over
var transitionEndHandler = function() {
domNode.removeEventListener("transitionend", transitionEndHandler);
delete domNode._slideTransitionHandler;
setAnimating(domNode, false);
$tw.utils.setStyle(domNode,[
{transition: "none"},
{transition: ""},
{marginBottom: ""},
{marginTop: ""},
{marginLeft: ""},
{marginRight: ""},
{paddingBottom: ""},
{paddingTop: ""},
{height: "auto"},
{opacity: ""}
{paddingLeft: ""},
{paddingRight: ""},
{height: ""},
{width: ""},
{opacity: ""},
{overflow: ""},
{willChange: ""}
]);
delete domNode._slideTargetDimensions;
if(options.callback) {
options.callback();
}
},duration);
// Set up the initial position of the element
};
// Store handler reference for potential interruption
domNode._slideTransitionHandler = transitionEndHandler;
// Set up the initial position
$tw.utils.setStyle(domNode,[{transition: "none"}]);
var targets = domNode._slideTargetDimensions;
if(isHorizontal) {
// For horizontal slides, the approach depends on origin
if(origin === "right") {
// For right origin, element slides in from the right
$tw.utils.setStyle(domNode,[
{transition: "none"},
{marginLeft: targets.marginLeft + "px"},
{marginRight: (-targets.width - targets.marginLeft - targets.marginRight - targets.paddingLeft - targets.paddingRight) + "px"},
{paddingLeft: targets.paddingLeft + "px"},
{paddingRight: targets.paddingRight + "px"},
{width: targets.width + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "margin-right, opacity"}
]);
} else if(origin === "center") {
// For center origin, start with zero width and center margins
var totalWidth = targets.width + targets.paddingLeft + targets.paddingRight;
$tw.utils.setStyle(domNode,[
{marginLeft: (totalWidth / 2) + targets.marginLeft + "px"},
{marginRight: (totalWidth / 2) + targets.marginRight + "px"},
{paddingLeft: startState.paddingStart + "px"},
{paddingRight: startState.paddingEnd + "px"},
{width: startState.size + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "width, opacity, margin-left, margin-right, padding-left, padding-right"}
]);
} else {
// Default left origin
$tw.utils.setStyle(domNode,[
{marginLeft: startState.marginStart + "px"},
{marginRight: startState.marginEnd + "px"},
{paddingLeft: startState.paddingStart + "px"},
{paddingRight: startState.paddingEnd + "px"},
{width: startState.size + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "width, opacity, margin-left, margin-right, padding-left, padding-right"}
]);
}
} else {
// For vertical slides
if(origin === "bottom") {
// For bottom origin, use negative margin-top to hide the element
$tw.utils.setStyle(domNode,[
{marginTop: (-targets.height - targets.marginTop - targets.marginBottom - targets.paddingTop - targets.paddingBottom) + "px"},
{marginBottom: targets.marginBottom + "px"},
{paddingTop: targets.paddingTop + "px"},
{paddingBottom: targets.paddingBottom + "px"},
{height: targets.height + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "margin-top, opacity"}
]);
} else if(origin === "center") {
// For center origin, start with zero height and center margins
var totalHeight = targets.height + targets.paddingTop + targets.paddingBottom;
$tw.utils.setStyle(domNode,[
{marginTop: (totalHeight / 2) + targets.marginTop + "px"},
{marginBottom: (totalHeight / 2) + targets.marginBottom + "px"},
{paddingTop: startState.paddingStart + "px"},
{paddingBottom: startState.paddingEnd + "px"},
{height: startState.size + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "height, opacity, margin-top, margin-bottom, padding-top, padding-bottom"}
]);
} else {
// Default top origin
$tw.utils.setStyle(domNode,[
{marginTop: startState.marginStart + "px"},
{marginBottom: startState.marginEnd + "px"},
{paddingTop: startState.paddingStart + "px"},
{paddingBottom: startState.paddingEnd + "px"},
{height: startState.size + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "height, opacity, margin-top, margin-bottom, padding-top, padding-bottom"}
]);
}
}
$tw.utils.forceLayout(domNode);
// Add transition end listener
domNode.addEventListener("transitionend", transitionEndHandler);
// Transition to the final position
var easing = options.easing || "cubic-bezier(0.4, 0.0, 0.2, 1)";
if(isHorizontal) {
// Different transitions based on origin
if(origin === "right") {
// For right origin, we only animate margin-right
$tw.utils.setStyle(domNode,[
{transition: "margin-right " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginRight: targets.marginRight + "px"},
{opacity: "1"}
]);
} else if(origin === "center") {
// For center origin, animate width and margins
$tw.utils.setStyle(domNode,[
{transition: "margin-left " + duration + "ms " + easing + ", " +
"margin-right " + duration + "ms " + easing + ", " +
"padding-left " + duration + "ms " + easing + ", " +
"padding-right " + duration + "ms " + easing + ", " +
"width " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginLeft: targets.marginLeft + "px"},
{marginRight: targets.marginRight + "px"},
{paddingLeft: targets.paddingLeft + "px"},
{paddingRight: targets.paddingRight + "px"},
{width: targets.width + "px"},
{opacity: "1"}
]);
} else {
// Default left origin
$tw.utils.setStyle(domNode,[
{transition: "margin-left " + duration + "ms " + easing + ", " +
"margin-right " + duration + "ms " + easing + ", " +
"padding-left " + duration + "ms " + easing + ", " +
"padding-right " + duration + "ms " + easing + ", " +
"width " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginLeft: targets.marginLeft + "px"},
{marginRight: targets.marginRight + "px"},
{paddingLeft: targets.paddingLeft + "px"},
{paddingRight: targets.paddingRight + "px"},
{width: targets.width + "px"},
{opacity: "1"}
]);
}
} else {
// Different transitions based on origin for vertical
if(origin === "bottom") {
// For bottom origin, we only animate margin-top
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginTop: targets.marginTop + "px"},
{opacity: "1"}
]);
} else if(origin === "center") {
// For center origin, animate height and margins
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms " + easing + ", " +
"margin-bottom " + duration + "ms " + easing + ", " +
"padding-top " + duration + "ms " + easing + ", " +
"padding-bottom " + duration + "ms " + easing + ", " +
"height " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginTop: targets.marginTop + "px"},
{marginBottom: targets.marginBottom + "px"},
{paddingTop: targets.paddingTop + "px"},
{paddingBottom: targets.paddingBottom + "px"},
{height: targets.height + "px"},
{opacity: "1"}
]);
} else {
// Default top origin
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms " + easing + ", " +
"margin-bottom " + duration + "ms " + easing + ", " +
"padding-top " + duration + "ms " + easing + ", " +
"padding-bottom " + duration + "ms " + easing + ", " +
"height " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginBottom: targets.marginBottom + "px"},
{marginTop: targets.marginTop + "px"},
{paddingBottom: targets.paddingBottom + "px"},
{paddingTop: targets.paddingTop + "px"},
{height: targets.height + "px"},
{opacity: "1"}
]);
}
}
}
function slideClosed(domNode,options) {
options = options || {};
var duration = options.duration || $tw.utils.getAnimationDuration();
var direction = options.direction || "vertical";
var isHorizontal = direction === "horizontal";
var origin = options.origin || (isHorizontal ? "left" : "top");
// If currently animating, capture current state
var startState;
if(isAnimating(domNode)) {
startState = getCurrentDimensions(domNode, isHorizontal);
// Remove any existing transition end handlers
var oldHandler = domNode._slideTransitionHandler;
if(oldHandler) {
domNode.removeEventListener("transitionend", oldHandler);
}
// Adjust duration based on current progress
var progress = startState.opacity; // Use opacity as progress indicator
duration = Math.round(duration * progress);
} else {
// Normal starting state
startState = {
size: isHorizontal ? domNode.offsetWidth : domNode.offsetHeight,
marginStart: parseFloat(window.getComputedStyle(domNode)[isHorizontal ? "marginLeft" : "marginTop"]) || 0,
marginEnd: parseFloat(window.getComputedStyle(domNode)[isHorizontal ? "marginRight" : "marginBottom"]) || 0,
paddingStart: parseFloat(window.getComputedStyle(domNode)[isHorizontal ? "paddingLeft" : "paddingTop"]) || 0,
paddingEnd: parseFloat(window.getComputedStyle(domNode)[isHorizontal ? "paddingRight" : "paddingBottom"]) || 0,
opacity: 1
};
}
// Mark as animating
setAnimating(domNode, true);
// Clear the properties when animation is over
var transitionEndHandler = function() {
domNode.removeEventListener("transitionend", transitionEndHandler);
delete domNode._slideTransitionHandler;
setAnimating(domNode, false);
$tw.utils.setStyle(domNode,[
{transition: ""},
{marginBottom: ""},
{marginTop: ""},
{marginLeft: ""},
{marginRight: ""},
{paddingBottom: ""},
{paddingTop: ""},
{paddingLeft: ""},
{paddingRight: ""},
{height: ""},
{width: ""},
{opacity: ""},
{overflow: ""},
{willChange: ""}
]);
delete domNode._slideTargetDimensions;
if(options.callback) {
options.callback();
}
};
// Store handler reference
domNode._slideTransitionHandler = transitionEndHandler;
// Set up the initial position
$tw.utils.setStyle(domNode,[{transition: "none"}]);
if(isHorizontal) {
$tw.utils.setStyle(domNode,[
{width: startState.size + "px"},
{marginLeft: startState.marginStart + "px"},
{marginRight: startState.marginEnd + "px"},
{paddingLeft: startState.paddingStart + "px"},
{paddingRight: startState.paddingEnd + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "width, opacity, margin-left, margin-right, padding-left, padding-right"}
]);
} else {
$tw.utils.setStyle(domNode,[
{height: startState.size + "px"},
{marginTop: startState.marginStart + "px"},
{marginBottom: startState.marginEnd + "px"},
{paddingTop: startState.paddingStart + "px"},
{paddingBottom: startState.paddingEnd + "px"},
{opacity: startState.opacity},
{overflow: "hidden"},
{willChange: "height, opacity, margin-top, margin-bottom, padding-top, padding-bottom"}
]);
}
$tw.utils.forceLayout(domNode);
// Add transition end listener
domNode.addEventListener("transitionend", transitionEndHandler);
// Transition to the final position
var easing = options.easing || "cubic-bezier(0.4, 0.0, 0.2, 1)";
// Get computed style for calculating negative margins
var computedStyle = window.getComputedStyle(domNode);
var currentWidth = domNode.offsetWidth;
var currentHeight = domNode.offsetHeight;
var marginLeft = parseInt(computedStyle.marginLeft, 10) || 0;
var marginRight = parseInt(computedStyle.marginRight, 10) || 0;
var marginTop = parseInt(computedStyle.marginTop, 10) || 0;
var marginBottom = parseInt(computedStyle.marginBottom, 10) || 0;
var paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0;
var paddingRight = parseInt(computedStyle.paddingRight, 10) || 0;
var paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
var paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
if(isHorizontal) {
// Different transitions based on origin
if(origin === "right") {
// For right origin, slide out to the right using negative margin-right
var totalWidth = currentWidth + marginLeft + marginRight + paddingLeft + paddingRight;
$tw.utils.setStyle(domNode,[
{transition: "margin-right " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginRight: (-totalWidth) + "px"},
{opacity: "0"}
]);
} else if(origin === "center") {
// For center origin, collapse width and expand margins
var halfWidth = (currentWidth + paddingLeft + paddingRight) / 2;
$tw.utils.setStyle(domNode,[
{transition: "margin-left " + duration + "ms " + easing + ", " +
"margin-right " + duration + "ms " + easing + ", " +
"padding-left " + duration + "ms " + easing + ", " +
"padding-right " + duration + "ms " + easing + ", " +
"width " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginLeft: (marginLeft + halfWidth) + "px"},
{marginRight: (marginRight + halfWidth) + "px"},
{paddingLeft: "0px"},
{paddingRight: "0px"},
{width: "0px"},
{opacity: "0"}
]);
} else {
// Default left origin
$tw.utils.setStyle(domNode,[
{transition: "margin-left " + duration + "ms " + easing + ", " +
"margin-right " + duration + "ms " + easing + ", " +
"padding-left " + duration + "ms " + easing + ", " +
"padding-right " + duration + "ms " + easing + ", " +
"width " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginLeft: "0px"},
{marginRight: "0px"},
{paddingLeft: "0px"},
{paddingRight: "0px"},
{width: "0px"},
{opacity: "0"}
]);
}
} else {
// Different transitions based on origin for vertical
if(origin === "bottom") {
// For bottom origin, slide out to the bottom using negative margin-top
var totalHeight = currentHeight + marginTop + marginBottom + paddingTop + paddingBottom;
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginTop: (-totalHeight) + "px"},
{opacity: "0"}
]);
} else if(origin === "center") {
// For center origin, collapse height and expand margins
var halfHeight = (currentHeight + paddingTop + paddingBottom) / 2;
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms " + easing + ", " +
"margin-bottom " + duration + "ms " + easing + ", " +
"padding-top " + duration + "ms " + easing + ", " +
"padding-bottom " + duration + "ms " + easing + ", " +
"height " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginTop: (marginTop + halfHeight) + "px"},
{marginBottom: (marginBottom + halfHeight) + "px"},
{paddingTop: "0px"},
{paddingBottom: "0px"},
{height: "0px"},
{opacity: "0"}
]);
} else {
// Default top origin
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms " + easing + ", " +
"margin-bottom " + duration + "ms " + easing + ", " +
"padding-top " + duration + "ms " + easing + ", " +
"padding-bottom " + duration + "ms " + easing + ", " +
"height " + duration + "ms " + easing + ", " +
"opacity " + duration + "ms " + easing},
{marginTop: "0px"},
{marginBottom: "0px"},
{paddingTop: "0px"},
@ -44,67 +549,343 @@ function slideOpen(domNode,options) {
{height: "0px"},
{opacity: "0"}
]);
$tw.utils.forceLayout(domNode);
// Transition to the final position
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms ease-in-out, " +
"margin-bottom " + duration + "ms ease-in-out, " +
"padding-top " + duration + "ms ease-in-out, " +
"padding-bottom " + duration + "ms ease-in-out, " +
"height " + duration + "ms ease-in-out, " +
"opacity " + duration + "ms ease-in-out"},
{marginBottom: currMarginBottom + "px"},
{marginTop: currMarginTop + "px"},
{paddingBottom: currPaddingBottom + "px"},
{paddingTop: currPaddingTop + "px"},
{height: currHeight + "px"},
{opacity: "1"}
]);
}
}
}
function slideClosed(domNode,options) {
// Transform-based versions with interruption support
function slideOpenTransform(domNode,options) {
options = options || {};
var duration = options.duration || $tw.utils.getAnimationDuration(),
currHeight = domNode.offsetHeight;
// Clear the properties we've set when the animation is over
setTimeout(function() {
var duration = options.duration || $tw.utils.getAnimationDuration();
var direction = options.direction || "vertical";
var isHorizontal = direction === "horizontal";
// Get current transform if animating
var startScale = 0;
var startOpacity = 0;
if(isAnimating(domNode)) {
var computedStyle = window.getComputedStyle(domNode);
var transform = computedStyle.transform;
if(transform && transform !== "none") {
var matrix = new DOMMatrix(transform);
startScale = isHorizontal ? matrix.a : matrix.d;
}
startOpacity = parseFloat(computedStyle.opacity) || 0;
// Remove existing handler
var oldHandler = domNode._slideTransitionHandler;
if(oldHandler) {
domNode.removeEventListener("transitionend", oldHandler);
}
}
// Mark as animating
setAnimating(domNode, true);
// Reset after animation
var transitionEndHandler = function() {
domNode.removeEventListener("transitionend", transitionEndHandler);
delete domNode._slideTransitionHandler;
setAnimating(domNode, false);
$tw.utils.setStyle(domNode,[
{transition: "none"},
{marginBottom: ""},
{marginTop: ""},
{paddingBottom: ""},
{paddingTop: ""},
{height: "auto"},
{opacity: ""}
{transition: ""},
{transform: ""},
{transformOrigin: ""},
{opacity: ""},
{willChange: ""}
]);
if(options.callback) {
options.callback();
}
},duration);
// Set up the initial position of the element
};
domNode._slideTransitionHandler = transitionEndHandler;
// Set initial state
var transformOrigin;
if(isHorizontal) {
transformOrigin = (options.origin || "left") + " center";
} else {
transformOrigin = "center " + (options.origin || "top");
}
$tw.utils.setStyle(domNode,[
{height: currHeight + "px"},
{transition: "none"},
{transform: isHorizontal ? "scale3d(" + startScale + ", 1, 1)" : "scale3d(1, " + startScale + ", 1)"},
{transformOrigin: transformOrigin},
{opacity: startOpacity},
{willChange: "transform, opacity"}
]);
$tw.utils.forceLayout(domNode);
// Add transition end listener
domNode.addEventListener("transitionend", transitionEndHandler);
// Animate to final state
var easing = options.easing || "cubic-bezier(0.4, 0.0, 0.2, 1)";
$tw.utils.setStyle(domNode,[
{transition: "transform " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{transform: "scale3d(1, 1, 1)"},
{opacity: "1"}
]);
$tw.utils.forceLayout(domNode);
// Transition to the final position
}
function slideClosedTransform(domNode,options) {
options = options || {};
var duration = options.duration || $tw.utils.getAnimationDuration();
var direction = options.direction || "vertical";
var isHorizontal = direction === "horizontal";
// Get current transform if animating
var startScale = 1;
var startOpacity = 1;
if(isAnimating(domNode)) {
var computedStyle = window.getComputedStyle(domNode);
var transform = computedStyle.transform;
if(transform && transform !== "none") {
var matrix = new DOMMatrix(transform);
startScale = isHorizontal ? matrix.a : matrix.d;
}
startOpacity = parseFloat(computedStyle.opacity) || 1;
// Adjust duration based on progress
duration = Math.round(duration * startOpacity);
// Remove existing handler
var oldHandler = domNode._slideTransitionHandler;
if(oldHandler) {
domNode.removeEventListener("transitionend", oldHandler);
}
}
// Mark as animating
setAnimating(domNode, true);
// Reset after animation
var transitionEndHandler = function() {
domNode.removeEventListener("transitionend", transitionEndHandler);
delete domNode._slideTransitionHandler;
setAnimating(domNode, false);
$tw.utils.setStyle(domNode,[
{transition: "margin-top " + duration + "ms ease-in-out, " +
"margin-bottom " + duration + "ms ease-in-out, " +
"padding-top " + duration + "ms ease-in-out, " +
"padding-bottom " + duration + "ms ease-in-out, " +
"height " + duration + "ms ease-in-out, " +
"opacity " + duration + "ms ease-in-out"},
{marginTop: "0px"},
{marginBottom: "0px"},
{paddingTop: "0px"},
{paddingBottom: "0px"},
{height: "0px"},
{transition: ""},
{transform: ""},
{transformOrigin: ""},
{opacity: ""},
{willChange: ""}
]);
if(options.callback) {
options.callback();
}
};
domNode._slideTransitionHandler = transitionEndHandler;
// Set initial state
var transformOrigin;
if(isHorizontal) {
transformOrigin = (options.origin || "left") + " center";
} else {
transformOrigin = "center " + (options.origin || "top");
}
$tw.utils.setStyle(domNode,[
{transition: "none"},
{transform: isHorizontal ? "scale3d(" + startScale + ", 1, 1)" : "scale3d(1, " + startScale + ", 1)"},
{transformOrigin: transformOrigin},
{opacity: startOpacity},
{willChange: "transform, opacity"}
]);
$tw.utils.forceLayout(domNode);
// Add transition end listener
domNode.addEventListener("transitionend", transitionEndHandler);
// Animate to final state
var easing = options.easing || "cubic-bezier(0.4, 0.0, 0.2, 1)";
$tw.utils.setStyle(domNode,[
{transition: "transform " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{transform: isHorizontal ? "scale3d(0, 1, 1)" : "scale3d(1, 0, 1)"},
{opacity: "0"}
]);
}
// GPU-accelerated versions using clip-path and transforms
function slideOpenGPU(domNode, options) {
options = options || {};
var duration = options.duration || $tw.utils.getAnimationDuration();
var direction = options.direction || "vertical";
var isHorizontal = direction === "horizontal";
var origin = options.origin || (isHorizontal ? "left" : "top");
// Get dimensions for clip-path
var rect = domNode.getBoundingClientRect();
var width = rect.width;
var height = rect.height;
// Check if already animating
var startOpacity = 0;
if(isAnimating(domNode)) {
var computedStyle = window.getComputedStyle(domNode);
startOpacity = parseFloat(computedStyle.opacity) || 0;
// Remove existing handler
var oldHandler = domNode._slideTransitionHandler;
if(oldHandler) {
domNode.removeEventListener("transitionend", oldHandler);
}
}
// Mark as animating
setAnimating(domNode, true);
// Reset after animation
var transitionEndHandler = function() {
domNode.removeEventListener("transitionend", transitionEndHandler);
delete domNode._slideTransitionHandler;
setAnimating(domNode, false);
$tw.utils.setStyle(domNode,[
{transition: ""},
{clipPath: ""},
{transform: ""},
{opacity: ""},
{willChange: ""}
]);
if(options.callback) {
options.callback();
}
};
domNode._slideTransitionHandler = transitionEndHandler;
// Set initial clip-path based on origin
var initialClip, finalClip = "inset(0 0 0 0)";
if(isHorizontal) {
if(origin === "right") {
initialClip = "inset(0 0 0 100%)";
} else if(origin === "center") {
initialClip = "inset(0 50% 0 50%)";
} else {
initialClip = "inset(0 100% 0 0)";
}
} else {
if(origin === "bottom") {
initialClip = "inset(100% 0 0 0)";
} else if(origin === "center") {
initialClip = "inset(50% 0 50% 0)";
} else {
initialClip = "inset(0 0 100% 0)";
}
}
// Set initial state
$tw.utils.setStyle(domNode,[
{transition: "none"},
{clipPath: initialClip},
{transform: "translate3d(0, 0, 0)"},
{opacity: startOpacity},
{willChange: "clip-path, opacity"}
]);
$tw.utils.forceLayout(domNode);
// Add transition end listener
domNode.addEventListener("transitionend", transitionEndHandler);
// Animate to final state
var easing = options.easing || "cubic-bezier(0.4, 0.0, 0.2, 1)";
$tw.utils.setStyle(domNode,[
{transition: "clip-path " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{clipPath: finalClip},
{opacity: "1"}
]);
}
function slideClosedGPU(domNode, options) {
options = options || {};
var duration = options.duration || $tw.utils.getAnimationDuration();
var direction = options.direction || "vertical";
var isHorizontal = direction === "horizontal";
var origin = options.origin || (isHorizontal ? "left" : "top");
// Check if already animating
var startOpacity = 1;
if(isAnimating(domNode)) {
var computedStyle = window.getComputedStyle(domNode);
startOpacity = parseFloat(computedStyle.opacity) || 1;
duration = Math.round(duration * startOpacity);
// Remove existing handler
var oldHandler = domNode._slideTransitionHandler;
if(oldHandler) {
domNode.removeEventListener("transitionend", oldHandler);
}
}
// Mark as animating
setAnimating(domNode, true);
// Reset after animation
var transitionEndHandler = function() {
domNode.removeEventListener("transitionend", transitionEndHandler);
delete domNode._slideTransitionHandler;
setAnimating(domNode, false);
$tw.utils.setStyle(domNode,[
{transition: ""},
{clipPath: ""},
{transform: ""},
{opacity: ""},
{willChange: ""}
]);
if(options.callback) {
options.callback();
}
};
domNode._slideTransitionHandler = transitionEndHandler;
// Set final clip-path based on origin
var finalClip;
if(isHorizontal) {
if(origin === "right") {
finalClip = "inset(0 0 0 100%)";
} else if(origin === "center") {
finalClip = "inset(0 50% 0 50%)";
} else {
finalClip = "inset(0 100% 0 0)";
}
} else {
if(origin === "bottom") {
finalClip = "inset(100% 0 0 0)";
} else if(origin === "center") {
finalClip = "inset(50% 0 50% 0)";
} else {
finalClip = "inset(0 0 100% 0)";
}
}
// Set initial state
$tw.utils.setStyle(domNode,[
{transition: "none"},
{clipPath: "inset(0 0 0 0)"},
{transform: "translate3d(0, 0, 0)"},
{opacity: startOpacity},
{willChange: "clip-path, opacity"}
]);
$tw.utils.forceLayout(domNode);
// Add transition end listener
domNode.addEventListener("transitionend", transitionEndHandler);
// Animate to final state
var easing = options.easing || "cubic-bezier(0.4, 0.0, 0.2, 1)";
$tw.utils.setStyle(domNode,[
{transition: "clip-path " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{clipPath: finalClip},
{opacity: "0"}
]);
}
exports.slide = {
open: slideOpen,
close: slideClosed
close: slideClosed,
openTransform: slideOpenTransform,
closeTransform: slideClosedTransform,
openGPU: slideOpenGPU,
closeGPU: slideClosedGPU
};

View file

@ -120,8 +120,33 @@ RevealWidget.prototype.execute = function() {
this["default"] = this.getAttribute("default","");
this.animate = this.getAttribute("animate","no");
this.retain = this.getAttribute("retain","no");
this.openAnimation = this.animate === "no" ? undefined : "open";
this.closeAnimation = this.animate === "no" ? undefined : "close";
// Animation type configuration
this.animationType = this.getAttribute("animationType","default"); // default, transform, gpu
// Set animation function names based on type
if(this.animate === "no") {
this.openAnimation = undefined;
this.closeAnimation = undefined;
} else {
// Use animation type to determine implementation
switch(this.animationType) {
case "transform":
this.openAnimation = "openTransform";
this.closeAnimation = "closeTransform";
break;
case "gpu":
this.openAnimation = "openGPU";
this.closeAnimation = "closeGPU";
break;
default:
this.openAnimation = "open";
this.closeAnimation = "close";
break;
}
}
this.animationDuration = parseInt(this.getAttribute("animationDuration") || $tw.utils.getAnimationDuration());
this.animationDirection = this.getAttribute("animationDirection");
this.animationOrigin = this.getAttribute("animationOrigin");
this.animationEasing = this.getAttribute("animationEasing");
this.updatePopupPosition = this.getAttribute("updatePopupPosition","no") === "yes";
// Compute the title of the state tiddler and read it
this.stateTiddlerTitle = this.state;
@ -211,7 +236,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
RevealWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.state || changedAttributes.type || changedAttributes.text || changedAttributes.position || changedAttributes.positionAllowNegative || changedAttributes["default"] || changedAttributes.animate || changedAttributes.stateTitle || changedAttributes.stateField || changedAttributes.stateIndex) {
if(changedAttributes.state || changedAttributes.type || changedAttributes.text || changedAttributes.position || changedAttributes.positionAllowNegative || changedAttributes["default"] || changedAttributes.animate || changedAttributes.stateTitle || changedAttributes.stateField || changedAttributes.stateIndex || changedAttributes.animationOrigin || changedAttributes.animationType || changedAttributes.animationEasing) {
this.refreshSelf();
return true;
} else {
@ -233,6 +258,9 @@ RevealWidget.prototype.refresh = function(changedTiddlers) {
if(changedAttributes["class"]) {
this.assignDomNodeClasses();
}
if(changedAttributes.animationDuration || changedTiddlers["$:/config/AnimationDuration"]) {
this.animationDuration = parseInt(this.getAttribute("animationDuration") || $tw.utils.getAnimationDuration());
}
return this.refreshChildren(changedTiddlers);
}
};
@ -259,15 +287,42 @@ RevealWidget.prototype.updateState = function() {
}
if(this.isOpen) {
domNode.removeAttribute("hidden");
$tw.anim.perform(this.openAnimation,domNode);
var animOptions = {};
if(this.animationDuration) {
animOptions.duration = this.animationDuration;
}
if(this.animationDirection) {
animOptions.direction = this.animationDirection;
}
if(this.animationOrigin) {
animOptions.origin = this.animationOrigin;
}
if(this.animationEasing) {
animOptions.easing = this.animationEasing;
}
$tw.anim.perform(this.openAnimation,domNode,animOptions);
} else {
$tw.anim.perform(this.closeAnimation,domNode,{callback: function() {
var animOptions = {};
if(this.animationDuration) {
animOptions.duration = this.animationDuration;
}
if(this.animationDirection) {
animOptions.direction = this.animationDirection;
}
if(this.animationOrigin) {
animOptions.origin = this.animationOrigin;
}
if(this.animationEasing) {
animOptions.easing = this.animationEasing;
}
animOptions.callback = function() {
//make sure that the state hasn't changed during the close animation
self.readState()
if(!self.isOpen) {
domNode.setAttribute("hidden","true");
}
}});
};
$tw.anim.perform(self.closeAnimation,domNode,animOptions);
}
};

View file

@ -10,7 +10,7 @@ $:/config/SideBarSegments/Visibility/$(listItem)$
<div class="tc-sidebar-header">
<$reveal state="$:/state/sidebar" type="match" text="yes" default="yes" retain="yes" animate="yes">
<$reveal state="$:/state/sidebar" type="match" text="yes" default="yes" retain="yes" animate="yes" animationDirection="horizontal" animationType="transform" animationOrigin="right" animationEasing="ease-in-out">
<$list filter="[all[shadows+tiddlers]tag[$:/tags/SideBarSegment]!has[draft.of]]" variable="listItem">

View file

@ -1,7 +1,7 @@
caption: reveal
created: 20131024141900000
jeremy: tiddlywiki
modified: 20250211091937860
modified: 20250809080611204
tags: Widgets
title: RevealWidget
type: text/vnd.tiddlywiki
@ -37,6 +37,11 @@ The content of the `<$reveal>` widget is displayed according to the rules given
|positionAllowNegative |Set to "yes" to prevent computed popup positions from being clamped to be above zero |
|default |Default value to use when the state tiddler is missing |
|animate |Set to "yes" to animate opening and closure (defaults to "no"; requires "retain" to be set to "yes") |
|animationType |<<.from-version "5.4.0">>The type of animation implementation to use: ''default'', ''transform'', or ''gpu'' (defaults to "default") |
|animationDuration |<<.from-version "5.4.0">>Duration of the animation in milliseconds (defaults to the global animation duration) |
|animationDirection |<<.from-version "5.4.0">>Direction of the animation effect: ''horizontal'' or ''vertical'' (defaults to "vertical") |
|animationOrigin |<<.from-version "5.4.0">>Origin point for the animation (e.g., "left", "right", "top", "bottom") |
|animationEasing |<<.from-version "5.4.0">>Easing function for the animation |
|retain |Set to "yes" to force the content to be retained even when hidden (defaults to "no") |
|updatePopupPosition|<<.from-version "5.1.23">>Set to "yes" to update the popup position when the state tiddler is updated (defaults to "no")|
@ -45,6 +50,8 @@ This is useful for edge-cases where titles may contain characters that are used
<<.tip """Retaining the content when hidden can give poor performance since the hidden content requires refresh processing even though it is not displayed. On the other hand, the content can be revealed much more quickly. Note that setting ''animate="yes"'' will also require ''retain="yes"''""">>
<<.tip """Animations of the reveal widget work only if both ''animate="yes"'' AND ''retain="yes"'' are set. The retain attribute is required because animations need the content to be present in the DOM even when hidden.""">>
! Examples
<<testcase TestCases/RevealWidget/SimpleReveal>>