fix(app): contain embedded media inside its box instead of overflowing (#1871)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:35:15 +02:00
1 parent 5834e32aa5
commit eda29c0e34
1 file changed
+61 -72
@@ -23,15 +23,56 @@ interface DimensionResult {
scale: number;
}
export const MEDIA_MAX_WIDTH = 550;
export const MEDIA_MAX_HEIGHT = 350;
const DEFAULT_OPTIONS: Required<DimensionOptions> = {
maxWidth: 550,
maxHeight: 400,
maxWidth: MEDIA_MAX_WIDTH,
maxHeight: MEDIA_MAX_HEIGHT,
preserve: false,
forceScale: false,
aspectRatio: true,
responsive: true,
};
export interface FitMediaInput extends MediaDimensions {
maxWidth: number;
maxHeight: number;
minWidth?: number;
minHeight?: number;
}
export function fitMediaWithinBounds({
width,
height,
maxWidth,
maxHeight,
minWidth = 0,
minHeight = 0,
}: FitMediaInput): MediaDimensions {
if (width === maxWidth && height === maxHeight) {
return {width, height};
}
const widthRatio = width > maxWidth ? maxWidth / width : 1;
let fittedWidth = Math.max(Math.round(width * widthRatio), minWidth);
let fittedHeight = Math.max(Math.round(height * widthRatio), minHeight);
const heightRatio = fittedHeight > maxHeight ? maxHeight / fittedHeight : 1;
fittedWidth = Math.max(Math.round(fittedWidth * heightRatio), minWidth);
fittedHeight = Math.max(Math.round(fittedHeight * heightRatio), minHeight);
return {width: fittedWidth, height: fittedHeight};
}
export function mediaContainRatio({width, height, maxWidth, maxHeight}: FitMediaInput): number {
const widthRatio = width > maxWidth ? maxWidth / width : 1;
const scaledHeight = Math.round(height * widthRatio);
const heightRatio = scaledHeight > maxHeight ? maxHeight / scaledHeight : 1;
return Math.min(widthRatio * heightRatio, 1);
}
export function mediaAspectRatioValue({width, height}: MediaDimensions): number {
return width / height;
}
export class MediaDimensionCalculator {
private options: Required<DimensionOptions>;
@@ -43,100 +84,49 @@ export class MediaDimensionCalculator {
const config = {...this.options, ...options};
const safeDimensions = this.normalizeDimensions(dimensions);
if (config.preserve) {
return this.preserveDimensions(safeDimensions);
return this.preserveDimensions(safeDimensions, config);
}
if (config.forceScale) {
return this.forceScaleDimensions(safeDimensions, config);
}
return this.calculateResponsiveDimensions(safeDimensions, config);
return this.containDimensions(safeDimensions, config);
}
public calculateImage(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'forceScale'>): DimensionResult {
return this.calculate(dimensions, {...options, forceScale: false});
public calculateImage(dimensions: MediaDimensions, options?: DimensionOptions): DimensionResult {
return this.calculate(dimensions, options);
}
public calculateVideo(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'preserve'>): DimensionResult {
return this.calculate(dimensions, {...options, preserve: false});
}
private preserveDimensions(dimensions: MediaDimensions): DimensionResult {
private preserveDimensions(dimensions: MediaDimensions, options: Required<DimensionOptions>): DimensionResult {
return {
style: {
width: remFromPx(dimensions.width),
aspectRatio: `${dimensions.width}/${dimensions.height}`,
maxWidth: '100%',
...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(dimensions)}),
},
dimensions: {...dimensions},
scale: 1,
};
}
private forceScaleDimensions(dimensions: MediaDimensions, options: Required<DimensionOptions>): DimensionResult {
private containDimensions(dimensions: MediaDimensions, options: Required<DimensionOptions>): DimensionResult {
const maxWidth = this.normalizeLimit(options.maxWidth, DEFAULT_OPTIONS.maxWidth);
const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight);
const scale = Math.min(1, maxWidth / dimensions.width, maxHeight / dimensions.height);
const scaledDimensions = {
width: Math.max(1, Math.round(dimensions.width * scale)),
height: Math.max(1, Math.round(dimensions.height * scale)),
};
const fitted = fitMediaWithinBounds({...dimensions, maxWidth, maxHeight});
const scale = mediaContainRatio({...dimensions, maxWidth, maxHeight});
return {
style: {
width: `min(100%, ${remFromPx(scaledDimensions.width)})`,
height: 'auto',
maxWidth: '100%',
maxHeight: '100%',
...(options.aspectRatio && {
aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`,
}),
maxWidth: `min(100%, ${remFromPx(fitted.width)})`,
maxHeight: remFromPx(fitted.height),
width: options.responsive ? '100%' : `min(100%, ${remFromPx(fitted.width)})`,
display: 'block',
...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(fitted)}),
},
dimensions: scaledDimensions,
dimensions: fitted,
scale,
};
}
private calculateResponsiveDimensions(
dimensions: MediaDimensions,
options: Required<DimensionOptions>,
): DimensionResult {
const maxWidth = this.normalizeLimit(options.maxWidth, DEFAULT_OPTIONS.maxWidth);
const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight);
const isPortrait = dimensions.height > dimensions.width;
let style: CSSProperties;
let scaledDimensions: MediaDimensions;
let scale: number;
if (isPortrait) {
const targetWidth = Math.max(1, Math.round((maxHeight * dimensions.width) / dimensions.height));
const maxAllowedWidth = Math.max(1, Math.min(dimensions.width, maxWidth, targetWidth));
scale = maxAllowedWidth / dimensions.width;
scaledDimensions = {
width: maxAllowedWidth,
height: Math.max(1, Math.round(dimensions.height * scale)),
};
style = {
maxWidth: `min(100%, ${remFromPx(maxAllowedWidth)})`,
width: options.responsive ? '100%' : `min(100%, ${remFromPx(maxAllowedWidth)})`,
...(options.aspectRatio && {
aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`,
}),
};
} else {
const scaleByWidth = maxWidth / dimensions.width;
const scaleByHeight = maxHeight / dimensions.height;
scale = Math.min(1, scaleByWidth, scaleByHeight);
scaledDimensions = {
width: Math.max(1, Math.round(dimensions.width * scale)),
height: Math.max(1, Math.round(dimensions.height * scale)),
};
style = {
maxWidth: `min(100%, ${remFromPx(maxWidth)})`,
width: options.responsive ? '100%' : `min(100%, ${remFromPx(scaledDimensions.width)})`,
...(options.aspectRatio && {
aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`,
}),
};
}
return {style, dimensions: scaledDimensions, scale};
}
private normalizeDimensions(dimensions: MediaDimensions): MediaDimensions {
return {
width: this.normalizeLimit(dimensions.width, 1),
@@ -157,9 +147,8 @@ export class MediaDimensionCalculator {
maxWidth = DEFAULT_OPTIONS.maxWidth,
maxHeight = DEFAULT_OPTIONS.maxHeight,
): [number, number] {
const calculator = new MediaDimensionCalculator();
const result = calculator.calculate({width, height}, {maxWidth, maxHeight, forceScale: true});
return [result.dimensions.width, result.dimensions.height];
const fitted = fitMediaWithinBounds({width, height, maxWidth, maxHeight});
return [fitted.width, fitted.height];
}
}