diff --git a/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js b/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js
new file mode 100644
index 00000000000..448cf41a443
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js
@@ -0,0 +1,60 @@
+import { withPluginApi } from "discourse/lib/plugin-api";
+
+// Browsers automatically calculate an aspect ratio based on the width/height attributes of an `
{
+ element.querySelectorAll("img").forEach((img) => {
+ const declaredHeight = parseFloat(img.getAttribute("height"));
+ const declaredWidth = parseFloat(img.getAttribute("width"));
+
+ if (
+ isNaN(declaredHeight) ||
+ isNaN(declaredWidth) ||
+ img.style.aspectRatio
+ ) {
+ return;
+ }
+
+ if (supportsAspectRatio) {
+ img.style.setProperty(
+ "aspect-ratio",
+ `${declaredWidth} / ${declaredHeight}`
+ );
+ } else {
+ // For older browsers (e.g. iOS < 15), we need to apply the aspect ratio manually.
+ // It's not perfect, because it won't recompute on browser resize.
+ // This property is consumed in `topic-post.scss` for responsive images only.
+ // It's a no-op for non-responsive images.
+ const calculatedHeight =
+ img.width / (declaredWidth / declaredHeight);
+
+ img.style.setProperty(
+ "--calculated-height",
+ `${calculatedHeight}px`
+ );
+ }
+ });
+ },
+ { id: "image-aspect-ratio" }
+ );
+ },
+
+ initialize() {
+ withPluginApi("1.2.0", this.initWithApi);
+ },
+};
diff --git a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js
index a5209010155..367fcf340e4 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js
@@ -1,4 +1,8 @@
-import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers";
+import {
+ acceptance,
+ normalizeHtml,
+ queryAll,
+} from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { IMAGE_VERSION as v } from "pretty-text/emoji/version";
@@ -12,8 +16,10 @@ acceptance("Emoji", function (needs) {
await fillIn(".d-editor-input", "this is an emoji :blonde_woman:");
assert.strictEqual(
- queryAll(".d-editor-preview:visible").html().trim(),
- `
this is an emoji 
`
+ normalizeHtml(queryAll(".d-editor-preview:visible").html().trim()),
+ normalizeHtml(
+ `this is an emoji 
`
+ )
);
});
@@ -22,9 +28,12 @@ acceptance("Emoji", function (needs) {
await click("#topic-footer-buttons .btn.create");
await fillIn(".d-editor-input", "this is an emoji :blonde_woman:t5:");
+
assert.strictEqual(
- queryAll(".d-editor-preview:visible").html().trim(),
- `this is an emoji 
`
+ normalizeHtml(queryAll(".d-editor-preview:visible").html().trim()),
+ normalizeHtml(
+ `this is an emoji 
`
+ )
);
});
});
diff --git a/app/assets/javascripts/discourse/tests/acceptance/image-aspect-ratio-test.js b/app/assets/javascripts/discourse/tests/acceptance/image-aspect-ratio-test.js
new file mode 100644
index 00000000000..b5bbf090665
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/image-aspect-ratio-test.js
@@ -0,0 +1,12 @@
+import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
+import { visit } from "@ember/test-helpers";
+import { test } from "qunit";
+
+acceptance("Image aspect ratio", function () {
+ test("it applies the aspect ratio", async function (assert) {
+ await visit("/t/2480");
+ const image = query("#post_3 img[src='/assets/logo.png']");
+
+ assert.strictEqual(image.style.aspectRatio, "690 / 388");
+ });
+});
diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js
index ace7abce1de..a0602c737a9 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js
@@ -2,6 +2,7 @@ import {
acceptance,
count,
exists,
+ normalizeHtml,
query,
queryAll,
visible,
@@ -54,8 +55,13 @@ acceptance("User Drafts", function (needs) {
"meta"
);
assert.strictEqual(
- query(".user-stream-item:nth-child(3) .excerpt").innerHTML.trim(),
- `here goes a reply to a PM
`
+ normalizeHtml(
+ query(".user-stream-item:nth-child(3) .excerpt").innerHTML.trim()
+ ),
+ normalizeHtml(
+ `here goes a reply to a PM
`
+ ),
+ "shows the excerpt"
);
});
});
diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
index 5065e7f057d..d5c159776d9 100644
--- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
+++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
@@ -574,3 +574,12 @@ export async function paste(element, text, otherClipboardData = {}) {
await settled();
return e;
}
+
+// The order of attributes can vary in diffferent browsers. When comparing
+// HTML strings from the DOM, this function helps to normalize them to make
+// comparison work cross-browser
+export function normalizeHtml(html) {
+ const resultElement = document.createElement("template");
+ resultElement.innerHTML = html;
+ return resultElement.innerHTML;
+}
diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss
index 51358b0cf4b..7362d75b207 100644
--- a/app/assets/stylesheets/common/base/topic-post.scss
+++ b/app/assets/stylesheets/common/base/topic-post.scss
@@ -192,6 +192,11 @@ $quote-share-maxwidth: 150px;
img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) {
max-width: 100%;
height: auto;
+
+ @supports not (aspect-ratio: 1) {
+ // (see javascripts/discourse/app/initializers/image-aspect-ratio.js)
+ height: var(--calculated-height);
+ }
}
}