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 :blonde_woman:

` + normalizeHtml(queryAll(".d-editor-preview:visible").html().trim()), + normalizeHtml( + `

this is an emoji :blonde_woman:

` + ) ); }); @@ -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 :blonde_woman:t5:

` + normalizeHtml(queryAll(".d-editor-preview:visible").html().trim()), + normalizeHtml( + `

this is an emoji :blonde_woman:t5:

` + ) ); }); }); 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 :slight_smile:` + normalizeHtml( + query(".user-stream-item:nth-child(3) .excerpt").innerHTML.trim() + ), + normalizeHtml( + `here goes a reply to a PM :slight_smile:` + ), + "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); + } } }