DEV: Refactor CSS generators to Glimmer component (#26202)

There is no need to use an initializer and manually update the DOM when
a Glimmer component can do it and ensure that the DOM is updated as more
categories are being loaded (for example, when lazy loaded categories
are enabled).
This commit is contained in:
Bianca Nenciu 2024-03-21 18:49:41 +02:00 committed by GitHub
parent ca651532c6
commit 2b4eb36a97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 184 additions and 223 deletions

View File

@ -0,0 +1,87 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { getURLWithCDN } from "discourse-common/lib/get-url";
export default class DStyles extends Component {
@service session;
@service site;
get categoryColors() {
return [
":root {",
...this.site.categories.map(
(category) => `--category-${category.id}-color: #${category.color};`
),
"}",
].join("\n");
}
get categoryBackgrounds() {
const css = [];
const darkCss = [];
this.site.categories.forEach((category) => {
const lightUrl = category.uploaded_background?.url;
const darkUrl =
this.session.defaultColorSchemeIsDark || this.session.darkModeAvailable
? category.uploaded_background_dark?.url
: null;
const defaultUrl =
darkUrl && this.session.defaultColorSchemeIsDark ? darkUrl : lightUrl;
if (defaultUrl) {
const url = getURLWithCDN(defaultUrl);
css.push(
`body.category-${category.fullSlug} { background-image: url(${url}); }`
);
}
if (darkUrl && defaultUrl !== darkUrl) {
const url = getURLWithCDN(darkUrl);
darkCss.push(
`body.category-${category.fullSlug} { background-image: url(${url}); }`
);
}
});
if (darkCss.length > 0) {
css.push("@media (prefers-color-scheme: dark) {", ...darkCss, "}");
}
return css.join("\n");
}
get categoryBadges() {
const css = [];
this.site.categories.forEach((category) => {
css.push(
`.badge-category[data-category-id="${category.id}"] { ` +
`--category-badge-color: var(--category-${category.id}-color); ` +
`--category-badge-text-color: #${category.text_color}; ` +
`}`
);
if (category.isParent) {
css.push(
`.badge-category[data-parent-category-id="${category.id}"] { ` +
`--parent-category-badge-color: var(--category-${category.id}-color); ` +
`}`
);
}
});
return css.join("\n");
}
<template>
{{! template-lint-disable no-forbidden-elements }}
<style id="d-styles">
{{#if this.site.categories}}
{{this.categoryColors}}
{{this.categoryBackgrounds}}
{{this.categoryBadges}}
{{/if}}
</style>
</template>
}

View File

@ -1,55 +0,0 @@
import { getURLWithCDN } from "discourse-common/lib/get-url";
export default {
after: "register-hashtag-types",
initialize(owner) {
this.session = owner.lookup("service:session");
this.site = owner.lookup("service:site");
if (!this.site.categories?.length) {
return;
}
const css = [];
const darkCss = [];
this.site.categories.forEach((category) => {
const lightUrl = category.uploaded_background?.url;
const darkUrl =
this.session.defaultColorSchemeIsDark || this.session.darkModeAvailable
? category.uploaded_background_dark?.url
: null;
const defaultUrl =
darkUrl && this.session.defaultColorSchemeIsDark ? darkUrl : lightUrl;
if (defaultUrl) {
const url = getURLWithCDN(defaultUrl);
css.push(
`body.category-${category.fullSlug} { background-image: url(${url}); }`
);
}
if (darkUrl && defaultUrl !== darkUrl) {
const url = getURLWithCDN(darkUrl);
darkCss.push(
`body.category-${category.fullSlug} { background-image: url(${url}); }`
);
}
});
if (darkCss.length > 0) {
css.push("@media (prefers-color-scheme: dark) {", ...darkCss, "}");
}
const cssTag = document.createElement("style");
cssTag.type = "text/css";
cssTag.id = "category-background-css-generator";
cssTag.innerHTML = css.join("\n");
document.head.appendChild(cssTag);
},
teardown() {
document.querySelector("#category-background-css-generator")?.remove();
},
};

View File

@ -1,33 +0,0 @@
import { get } from "@ember/object";
import Category from "discourse/models/category";
export default {
after: "category-color-css-generator",
// This generates badge CSS for each category, which is used to render category-specific elements.
initialize(owner) {
this.site = owner.lookup("service:site");
// If the site is login_required and the user is anon there will be no categories preloaded.
if (!this.site.categories?.length) {
return;
}
const generatedCss = this.site.categories.map((category) => {
let parentCategory = Category.findById(
get(category, "parent_category_id")
);
let badgeCss = `.badge-category[data-category-id="${category.id}"] { --category-badge-color: var(--category-${category.id}-color); --category-badge-text-color: #${category.text_color}; }`;
if (parentCategory) {
badgeCss += `.badge-category[data-parent-category-id="${parentCategory.id}"] { --parent-category-badge-color: var(--category-${parentCategory.id}-color); }`;
}
return badgeCss;
});
const cssTag = document.createElement("style");
cssTag.type = "text/css";
cssTag.id = "category-badge-css-generator";
cssTag.innerHTML = generatedCss.join("\n");
document.head.appendChild(cssTag);
},
};

View File

@ -1,33 +0,0 @@
export default {
after: "register-hashtag-types",
/**
* This generates CSS variables for each category color,
* which can be used in themes to style category-specific elements.
*
* It is also used when styling hashtag icons, since they are colored
* based on the category color.
*/
initialize(owner) {
this.site = owner.lookup("service:site");
// If the site is login_required and the user is anon there will be no categories preloaded.
if (!this.site.categories?.length) {
return;
}
const generatedCssVariables = [
":root {",
...this.site.categories.map(
(category) => `--category-${category.id}-color: #${category.color};`
),
"}",
];
const cssTag = document.createElement("style");
cssTag.type = "text/css";
cssTag.id = "category-color-css-generator";
cssTag.innerHTML = generatedCssVariables.join("\n");
document.head.appendChild(cssTag);
},
};

View File

@ -1,7 +1,7 @@
import { getHashtagTypeClasses } from "discourse/lib/hashtag-type-registry"; import { getHashtagTypeClasses } from "discourse/lib/hashtag-type-registry";
export default { export default {
after: "category-color-css-generator", after: "register-hashtag-types",
/** /**
* This generates CSS classes for each hashtag type, * This generates CSS classes for each hashtag type,

View File

@ -1,3 +1,4 @@
import { tracked } from "@glimmer/tracking";
import EmberObject, { get } from "@ember/object"; import EmberObject, { get } from "@ember/object";
import { alias, sort } from "@ember/object/computed"; import { alias, sort } from "@ember/object/computed";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
@ -103,9 +104,12 @@ export default class Site extends RestModel.extend().reopenClass(Singleton) {
return result; return result;
} }
@tracked categories;
@alias("is_readonly") isReadOnly; @alias("is_readonly") isReadOnly;
@sort("categories", "topicCountDesc") categoriesByCount; @sort("categories", "topicCountDesc") categoriesByCount;
init() { init() {
super.init(...arguments); super.init(...arguments);

View File

@ -1,3 +1,5 @@
<DStyles />
<DiscourseRoot> <DiscourseRoot>
<a href="#main-container" id="skip-link">{{i18n "skip_to_main_content"}}</a> <a href="#main-container" id="skip-link">{{i18n "skip_to_main_content"}}</a>
<DDocument /> <DDocument />

View File

@ -1,81 +0,0 @@
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
acceptance("CSS Generator", function (needs) {
needs.user();
needs.site({
categories: [
{ id: 1, color: "ff0000", text_color: "ffffff", name: "category1" },
{ id: 2, color: "333", text_color: "ffffff", name: "category2" },
{
id: 4,
color: "2B81AF",
text_color: "ffffff",
parentCategory: { id: 1 },
name: "category3",
},
],
});
test("category CSS variables are generated", async function (assert) {
await visit("/");
const cssTag = document.querySelector("style#category-color-css-generator");
assert.equal(
cssTag.innerHTML,
":root {\n" +
"--category-1-color: #ff0000;\n" +
"--category-2-color: #333;\n" +
"--category-4-color: #2B81AF;\n" +
"}"
);
});
test("hashtag CSS classes are generated", async function (assert) {
await visit("/");
const cssTag = document.querySelector("style#hashtag-css-generator");
assert.equal(
cssTag.innerHTML,
".hashtag-category-badge { background-color: var(--primary-medium); }\n" +
".hashtag-color--category-1 { background-color: #ff0000; }\n" +
".hashtag-color--category-2 { background-color: #333; }\n" +
".hashtag-color--category-4 { background-color: #2B81AF; }"
);
});
test("category badge CSS variables are generated", async function (assert) {
await visit("/");
const cssTag = document.querySelector("style#category-badge-css-generator");
assert.equal(
cssTag.innerHTML,
'.badge-category[data-category-id="1"] { --category-badge-color: var(--category-1-color); --category-badge-text-color: #ffffff; }\n' +
'.badge-category[data-category-id="2"] { --category-badge-color: var(--category-2-color); --category-badge-text-color: #ffffff; }\n' +
'.badge-category[data-category-id="4"] { --category-badge-color: var(--category-4-color); --category-badge-text-color: #ffffff; }'
);
});
});
acceptance(
"CSS Generator | Anon user in login_required site",
function (needs) {
needs.site({ categories: null });
needs.settings({ login_required: true });
test("category CSS variables are not generated", async function (assert) {
await visit("/");
const cssTag = document.querySelector(
"style#category-color-css-generator"
);
assert.notOk(exists(cssTag));
});
test("category badge CSS variables are not generated", async function (assert) {
await visit("/");
const cssTag = document.querySelector(
"style#category-badge-css-generator"
);
assert.notOk(exists(cssTag));
});
}
);

View File

@ -54,22 +54,21 @@ const SITE_DATA = {
], ],
}; };
acceptance("Category Background CSS Generator", function (needs) { acceptance("DStyles - category backgrounds", function (needs) {
needs.user(); needs.user();
needs.site(SITE_DATA); needs.site(SITE_DATA);
test("CSS classes are generated", async function (assert) { test("CSS classes are generated", async function (assert) {
await visit("/"); await visit("/");
assert.equal( const css =
document.querySelector("#category-background-css-generator").innerHTML,
"body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" + "body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }" "body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }";
); assert.ok(document.querySelector("#d-styles").innerHTML.includes(css));
}); });
}); });
acceptance("Category Background CSS Generator (dark)", function (needs) { acceptance("DStyles - category backgrounds (dark)", function (needs) {
needs.user(); needs.user();
needs.site(SITE_DATA); needs.site(SITE_DATA);
@ -88,20 +87,19 @@ acceptance("Category Background CSS Generator (dark)", function (needs) {
test("CSS classes are generated", async function (assert) { test("CSS classes are generated", async function (assert) {
await visit("/"); await visit("/");
assert.equal( const css =
document.querySelector("#category-background-css-generator").innerHTML,
"body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" + "body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }\n" + "body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }\n" +
"@media (prefers-color-scheme: dark) {\n" + "@media (prefers-color-scheme: dark) {\n" +
"body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" + "body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }\n" + "body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }\n" +
"}" "}";
); assert.ok(document.querySelector("#d-styles").innerHTML.includes(css));
}); });
}); });
acceptance( acceptance(
"Category Background CSS Generator (dark is default)", "DStyles - category backgrounds (default theme is dark)",
function (needs) { function (needs) {
needs.user(); needs.user();
needs.site(SITE_DATA); needs.site(SITE_DATA);
@ -121,12 +119,51 @@ acceptance(
test("CSS classes are generated", async function (assert) { test("CSS classes are generated", async function (assert) {
await visit("/"); await visit("/");
assert.equal( const css =
document.querySelector("#category-background-css-generator").innerHTML,
"body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" + "body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" +
"body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" + "body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }" "body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }";
); assert.ok(document.querySelector("#d-styles").innerHTML.includes(css));
}); });
} }
); );
acceptance("DStyles - category badges", function (needs) {
needs.user();
needs.site({
categories: [
{ id: 1, color: "ff0000", text_color: "ffffff", name: "category1" },
{ id: 2, color: "333", text_color: "ffffff", name: "category2" },
{
id: 4,
color: "2B81AF",
text_color: "ffffff",
parentCategory: { id: 1 },
name: "category3",
},
],
});
test("category CSS variables are generated", async function (assert) {
await visit("/");
const css =
":root {\n" +
"--category-1-color: #ff0000;\n" +
"--category-2-color: #333;\n" +
"--category-4-color: #2B81AF;\n" +
"}";
assert.ok(document.querySelector("style#d-styles").innerHTML.includes(css));
});
test("category badge CSS variables are generated", async function (assert) {
await visit("/");
const css =
'.badge-category[data-category-id="1"] { --category-badge-color: var(--category-1-color); --category-badge-text-color: #ffffff; }\n' +
'.badge-category[data-category-id="2"] { --category-badge-color: var(--category-2-color); --category-badge-text-color: #ffffff; }\n' +
'.badge-category[data-category-id="4"] { --category-badge-color: var(--category-4-color); --category-badge-text-color: #ffffff; }';
assert.ok(document.querySelector("style#d-styles").innerHTML.includes(css));
});
});

View File

@ -0,0 +1,33 @@
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
acceptance("Hashtag CSS Generator", function (needs) {
needs.user();
needs.site({
categories: [
{ id: 1, color: "ff0000", text_color: "ffffff", name: "category1" },
{ id: 2, color: "333", text_color: "ffffff", name: "category2" },
{
id: 4,
color: "2B81AF",
text_color: "ffffff",
parentCategory: { id: 1 },
name: "category3",
},
],
});
test("classes are generated", async function (assert) {
await visit("/");
const cssTag = document.querySelector("style#hashtag-css-generator");
assert.equal(
cssTag.innerHTML,
".hashtag-category-badge { background-color: var(--primary-medium); }\n" +
".hashtag-color--category-1 { background-color: #ff0000; }\n" +
".hashtag-color--category-2 { background-color: #333; }\n" +
".hashtag-color--category-4 { background-color: #2B81AF; }"
);
});
});