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; scale: number;
} }
export const MEDIA_MAX_WIDTH = 550;
export const MEDIA_MAX_HEIGHT = 350;
const DEFAULT_OPTIONS: Required<DimensionOptions> = { const DEFAULT_OPTIONS: Required<DimensionOptions> = {
maxWidth: 550, maxWidth: MEDIA_MAX_WIDTH,
maxHeight: 400, maxHeight: MEDIA_MAX_HEIGHT,
preserve: false, preserve: false,
forceScale: false, forceScale: false,
aspectRatio: true, aspectRatio: true,
responsive: 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 { export class MediaDimensionCalculator {
private options: Required<DimensionOptions>; private options: Required<DimensionOptions>;
@@ -43,100 +84,49 @@ export class MediaDimensionCalculator {
const config = {...this.options, ...options}; const config = {...this.options, ...options};
const safeDimensions = this.normalizeDimensions(dimensions); const safeDimensions = this.normalizeDimensions(dimensions);
if (config.preserve) { if (config.preserve) {
return this.preserveDimensions(safeDimensions); return this.preserveDimensions(safeDimensions, config);
} }
if (config.forceScale) { return this.containDimensions(safeDimensions, config);
return this.forceScaleDimensions(safeDimensions, config);
}
return this.calculateResponsiveDimensions(safeDimensions, config);
} }
public calculateImage(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'forceScale'>): DimensionResult { public calculateImage(dimensions: MediaDimensions, options?: DimensionOptions): DimensionResult {
return this.calculate(dimensions, {...options, forceScale: false}); return this.calculate(dimensions, options);
} }
public calculateVideo(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'preserve'>): DimensionResult { public calculateVideo(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'preserve'>): DimensionResult {
return this.calculate(dimensions, {...options, preserve: false}); return this.calculate(dimensions, {...options, preserve: false});
} }
private preserveDimensions(dimensions: MediaDimensions): DimensionResult { private preserveDimensions(dimensions: MediaDimensions, options: Required<DimensionOptions>): DimensionResult {
return { return {
style: { style: {
width: remFromPx(dimensions.width), width: remFromPx(dimensions.width),
aspectRatio: `${dimensions.width}/${dimensions.height}`, maxWidth: '100%',
...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(dimensions)}),
}, },
dimensions: {...dimensions}, dimensions: {...dimensions},
scale: 1, 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 maxWidth = this.normalizeLimit(options.maxWidth, DEFAULT_OPTIONS.maxWidth);
const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight); const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight);
const scale = Math.min(1, maxWidth / dimensions.width, maxHeight / dimensions.height); const fitted = fitMediaWithinBounds({...dimensions, maxWidth, maxHeight});
const scaledDimensions = { const scale = mediaContainRatio({...dimensions, maxWidth, maxHeight});
width: Math.max(1, Math.round(dimensions.width * scale)),
height: Math.max(1, Math.round(dimensions.height * scale)),
};
return { return {
style: { style: {
width: `min(100%, ${remFromPx(scaledDimensions.width)})`, maxWidth: `min(100%, ${remFromPx(fitted.width)})`,
height: 'auto', maxHeight: remFromPx(fitted.height),
maxWidth: '100%', width: options.responsive ? '100%' : `min(100%, ${remFromPx(fitted.width)})`,
maxHeight: '100%', display: 'block',
...(options.aspectRatio && { ...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(fitted)}),
aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`,
}),
}, },
dimensions: scaledDimensions, dimensions: fitted,
scale, 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 { private normalizeDimensions(dimensions: MediaDimensions): MediaDimensions {
return { return {
width: this.normalizeLimit(dimensions.width, 1), width: this.normalizeLimit(dimensions.width, 1),
@@ -157,9 +147,8 @@ export class MediaDimensionCalculator {
maxWidth = DEFAULT_OPTIONS.maxWidth, maxWidth = DEFAULT_OPTIONS.maxWidth,
maxHeight = DEFAULT_OPTIONS.maxHeight, maxHeight = DEFAULT_OPTIONS.maxHeight,
): [number, number] { ): [number, number] {
const calculator = new MediaDimensionCalculator(); const fitted = fitMediaWithinBounds({width, height, maxWidth, maxHeight});
const result = calculator.calculate({width, height}, {maxWidth, maxHeight, forceScale: true}); return [fitted.width, fitted.height];
return [result.dimensions.width, result.dimensions.height];
} }
} }