This commit upgrades TypeScript to the latest version 5.3 and introduces `verbatimModuleSyntax` in line with the official Vue guide recommendatinos (vuejs/docs#2592). By enforcing `import type` for type-only imports, this commit improves code clarity and supports tooling optimization, ensuring imports are only bundled when necessary for runtime. Changes: - Bump TypeScript to 5.3.3 across the project. - Adjust import statements to utilize `import type` where applicable, promoting cleaner and more efficient code.
224 lines
6.5 KiB
TypeScript
224 lines
6.5 KiB
TypeScript
import { PlatformTimer } from '@/application/Common/Timing/PlatformTimer';
|
|
import type { Timer } from '@/application/Common/Timing/Timer';
|
|
|
|
export type AnimationFunction = (element: Element) => Promise<void>;
|
|
|
|
export function useExpandCollapseAnimation(
|
|
timer: Timer = PlatformTimer,
|
|
): {
|
|
readonly collapse: AnimationFunction;
|
|
readonly expand: AnimationFunction;
|
|
} {
|
|
return {
|
|
collapse: (element: Element) => animateCollapse(element, timer),
|
|
expand: (element: Element) => animateExpand(element, timer),
|
|
};
|
|
}
|
|
|
|
function animateCollapse(
|
|
element: Element,
|
|
timer: Timer,
|
|
): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
assertElementIsHTMLElement(element);
|
|
applyStyleMutations(element, (elementStyle) => {
|
|
const measuredStyles = captureTransitionDimensions(element, elementStyle);
|
|
setTransitionPropertiesToHidden(elementStyle);
|
|
hideOverflow(elementStyle);
|
|
triggerElementRepaint(element);
|
|
setTransitionPropertiesToElementDimensions(elementStyle, measuredStyles);
|
|
startTransition(elementStyle, timer).then(() => {
|
|
elementStyle.restoreOriginalStyles();
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function animateExpand(
|
|
element: Element,
|
|
timer: Timer,
|
|
): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
assertElementIsHTMLElement(element);
|
|
applyStyleMutations(element, (elementStyle) => {
|
|
const measuredStyles = captureTransitionDimensions(element, elementStyle);
|
|
setTransitionPropertiesToElementDimensions(elementStyle, measuredStyles);
|
|
hideOverflow(elementStyle);
|
|
triggerElementRepaint(element);
|
|
setTransitionPropertiesToHidden(elementStyle);
|
|
startTransition(elementStyle, timer).then(() => {
|
|
elementStyle.restoreOriginalStyles();
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
export const TRANSITION_DURATION_MILLISECONDS = 300;
|
|
|
|
const TRANSITION_EASING_FUNCTION = 'ease-in-out';
|
|
|
|
function startTransition(
|
|
styleMutator: ElementStyleMutator,
|
|
timer: Timer,
|
|
): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
styleMutator.changeStyle('transition', createTransitionStyleValue());
|
|
timer.setTimeout(() => resolve(), TRANSITION_DURATION_MILLISECONDS);
|
|
});
|
|
}
|
|
|
|
interface ElementStyleMutator {
|
|
readonly restoreOriginalStyles: () => void;
|
|
readonly changeStyle: (key: MutatedStyleProperty, value: string) => void;
|
|
}
|
|
|
|
function applyStyleMutations(
|
|
element: HTMLElement,
|
|
mutator: (elementStyle: ElementStyleMutator) => void,
|
|
) {
|
|
const originalStyles = getOriginalStyles(element);
|
|
const changeStyle = (key: MutatedStyleProperty, value: string) => {
|
|
element.style[key] = value;
|
|
};
|
|
mutator({
|
|
restoreOriginalStyles: () => restoreOriginalStyles(element, originalStyles),
|
|
changeStyle,
|
|
});
|
|
}
|
|
|
|
function setTransitionPropertiesToHidden(elementStyle: ElementStyleMutator): void {
|
|
TransitionedStyleProperties.forEach((key) => {
|
|
elementStyle.changeStyle(key, '0px');
|
|
});
|
|
}
|
|
|
|
function setTransitionPropertiesToElementDimensions(
|
|
elementStyle: ElementStyleMutator,
|
|
elementDimensions: TransitionStyleRecords,
|
|
): void {
|
|
Object.entries(elementDimensions).forEach(([key, value]) => {
|
|
elementStyle.changeStyle(key as AnimatedStyleProperty, value);
|
|
});
|
|
}
|
|
|
|
function hideOverflow(elementStyle: ElementStyleMutator): void {
|
|
elementStyle.changeStyle('overflow', 'hidden');
|
|
}
|
|
|
|
function createTransitionStyleValue(): string {
|
|
const transitions = new Array<string>();
|
|
TransitionedStyleProperties.forEach((key) => {
|
|
transitions.push(`${getCssStyleName(key)} ${TRANSITION_DURATION_MILLISECONDS}ms ${TRANSITION_EASING_FUNCTION}`);
|
|
});
|
|
return transitions.join(', ');
|
|
}
|
|
|
|
function captureTransitionDimensions(
|
|
element: Readonly<HTMLElement>,
|
|
styleMutator: ElementStyleMutator,
|
|
): TransitionStyleRecords {
|
|
let styles: TransitionStyleRecords | undefined;
|
|
executeActionWithTemporaryVisibility(
|
|
element.style,
|
|
styleMutator,
|
|
() => {
|
|
styles = {
|
|
height: `${element.offsetHeight}px`,
|
|
paddingTop: element.style.paddingTop || getElementComputedStylePropertyValue(element, 'padding-top'),
|
|
paddingBottom: element.style.paddingBottom || getElementComputedStylePropertyValue(element, 'padding-bottom'),
|
|
};
|
|
},
|
|
);
|
|
return styles as TransitionStyleRecords;
|
|
}
|
|
|
|
function triggerElementRepaint(
|
|
element: Readonly<HTMLElement>,
|
|
): void {
|
|
element.offsetHeight; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
|
}
|
|
|
|
function getElementComputedStylePropertyValue(element: Element, style: string) {
|
|
return getComputedStyle(element, null).getPropertyValue(style);
|
|
}
|
|
|
|
function executeActionWithTemporaryVisibility(
|
|
readableStyle: Readonly<CSSStyleDeclaration>,
|
|
styleMutator: ElementStyleMutator,
|
|
actionWhileRendered: () => void,
|
|
) {
|
|
const {
|
|
visibility: initialVisibility,
|
|
display: initialDisplay,
|
|
} = readableStyle;
|
|
styleMutator.changeStyle('visibility', 'hidden');
|
|
styleMutator.changeStyle('display', '');
|
|
try {
|
|
actionWhileRendered();
|
|
} finally {
|
|
styleMutator.changeStyle('visibility', initialVisibility);
|
|
styleMutator.changeStyle('display', initialDisplay);
|
|
}
|
|
}
|
|
|
|
function getOriginalStyles(element: HTMLElement): MutatedStyleProperties {
|
|
const records = {} as MutatedStyleProperties;
|
|
MutatedStylePropertiesDuringAnimation.forEach((key) => {
|
|
records[key] = element.style[key];
|
|
});
|
|
return records;
|
|
}
|
|
|
|
function restoreOriginalStyles(
|
|
element: HTMLElement,
|
|
originalStyles: MutatedStyleProperties,
|
|
): void {
|
|
Object.entries(originalStyles).forEach(([key, value]) => {
|
|
element.style[key as MutatedStyleProperty] = value;
|
|
});
|
|
}
|
|
|
|
function getCssStyleName(style: AnimatedStyleProperty): string {
|
|
const cssPropertyNames: TransitionStyleRecords = {
|
|
height: 'height',
|
|
paddingTop: 'padding-top',
|
|
paddingBottom: 'padding-bottom',
|
|
};
|
|
return cssPropertyNames[style];
|
|
}
|
|
|
|
function assertElementIsHTMLElement(
|
|
element: Element,
|
|
): asserts element is HTMLElement {
|
|
if (!element) {
|
|
throw new Error('Element was not found');
|
|
}
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error('Element is not an HTMLElement');
|
|
}
|
|
}
|
|
|
|
const TransitionedStyleProperties = [
|
|
'height',
|
|
'paddingTop',
|
|
'paddingBottom',
|
|
] as const;
|
|
|
|
const MutatedStylePropertiesDuringAnimation = [
|
|
...TransitionedStyleProperties,
|
|
'transition',
|
|
'overflow',
|
|
'visibility',
|
|
'display',
|
|
] as const;
|
|
|
|
type MutatedStyleProperty = typeof MutatedStylePropertiesDuringAnimation[number];
|
|
|
|
export type MutatedStyleProperties = Record<MutatedStyleProperty, string>;
|
|
|
|
type AnimatedStyleProperty = typeof TransitionedStyleProperties[number];
|
|
|
|
type TransitionStyleRecords = Record<AnimatedStyleProperty, string>;
|