diff --git a/core/modules/utils/dom/animations/slide.js b/core/modules/utils/dom/animations/slide.js index 2057c4460..21cb25e00 100644 --- a/core/modules/utils/dom/animations/slide.js +++ b/core/modules/utils/dom/animations/slide.js @@ -9,102 +9,883 @@ 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 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() { + 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), + 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 - $tw.utils.setStyle(domNode,[ - {transition: "none"}, - {marginTop: "0px"}, - {marginBottom: "0px"}, - {paddingTop: "0px"}, - {paddingBottom: "0px"}, - {height: "0px"}, - {opacity: "0"} - ]); + }; + + // 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,[ + {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 - $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"} - ]); + 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(), - 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"; + 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: "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(); + } + }; + + // 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"}, + {paddingBottom: "0px"}, + {height: "0px"}, + {opacity: "0"} + ]); + } + } +} + +// Transform-based versions with interruption support +function slideOpenTransform(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 = 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: ""}, + {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: ""}, + {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: "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: "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 }; diff --git a/core/modules/widgets/reveal.js b/core/modules/widgets/reveal.js index 14d5ff0d7..1a39c5751 100755 --- a/core/modules/widgets/reveal.js +++ b/core/modules/widgets/reveal.js @@ -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); } }; diff --git a/core/ui/PageTemplate/sidebar.tid b/core/ui/PageTemplate/sidebar.tid index 642289418..76c871f43 100644 --- a/core/ui/PageTemplate/sidebar.tid +++ b/core/ui/PageTemplate/sidebar.tid @@ -10,7 +10,7 @@ $:/config/SideBarSegments/Visibility/$(listItem)$