mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 10:20:58 -06:00
FEATURE: Add CSS class generation for category colors and hashtags (#20951)
This commit adds a system to generate CSS variables and classes for categories and hashtags, which will be used in an effort to remove baked icons for hashtags and add color to those icons. This is in two parts. First I added an initializer generate a category color CSS variable style tag in the head tag that looks like this: ```css :root { --category-1-color: #0088CC; --category-2-color: #808281; --category-3-color: #E45735; --category-4-color: #A461EF; --category-5-color: #ee56c9; --category-6-color: #da28c2; --category-7-color: #ab8b0a; --category-8-color: #45da37; ... } ``` The number is the category ID. This only generates CSS variables for categories the user can access based on `site.categories`. If you need the parent color variable you can just use the `category.parentCategory.id` to get it. Then, I added an initializer to generate a hashtag CSS style tag using these variables. Only the category and channel hashtags need this, the category one generates the background-gradient needed for the swatch, and the channel just generates a color for the icon. This is done in an extendable way using the new `api.registerHashtagType` JS plugin API: ```css hashtag-color--category-1 { background: linear-gradient(90deg, var(--category-1-color) 50%, var(--category-1-color) 50%); } hashtag-color--category-2 { background: linear-gradient(90deg, var(--category-2-color) 50%, var(--category-2-color) 50%); } hashtag-color--category-5 { background: linear-gradient(90deg, var(--category-5-color) 50%, var(--category-4-color) 50%); } ... .hashtag-color--channel-4 { color: var(--category-12-color); } .hashtag-color--channel-92 { color: var(--category-24-color); } ``` Note if a category has a parent, its color is used in the gradient correctly. The numbers here are again IDs (e.g. channel ID, category ID) and the channel’s chatable ID is used to find the category color variable.
This commit is contained in:
parent
b72282123b
commit
6ad9e4ad06
@ -0,0 +1,29 @@
|
||||
export default {
|
||||
name: "category-color-css-generator",
|
||||
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(container) {
|
||||
this.site = container.lookup("service:site");
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import { getHashtagTypeClasses } from "discourse/lib/hashtag-autocomplete";
|
||||
|
||||
export default {
|
||||
name: "hashtag-css-generator",
|
||||
after: "category-color-css-generator",
|
||||
|
||||
/**
|
||||
* This generates CSS classes for each hashtag type,
|
||||
* which are used to color the hashtag icons in the composer,
|
||||
* cooked posts, and the sidebar.
|
||||
*
|
||||
* Each type has its own corresponding class, which is registered
|
||||
* with the hastag type via api.registerHashtagType. The default
|
||||
* ones in core are CategoryHashtagType and TagHashtagType.
|
||||
*/
|
||||
initialize(container) {
|
||||
let generatedCssClasses = [];
|
||||
|
||||
Object.values(getHashtagTypeClasses()).forEach((hashtagTypeClass) => {
|
||||
const hashtagType = new hashtagTypeClass(container);
|
||||
hashtagType.preloadedData.forEach((model) => {
|
||||
generatedCssClasses = generatedCssClasses.concat(
|
||||
hashtagType.generateColorCssClasses(model)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const cssTag = document.createElement("style");
|
||||
cssTag.type = "text/css";
|
||||
cssTag.id = "hashtag-css-generator";
|
||||
cssTag.innerHTML = generatedCssClasses.join("\n");
|
||||
document.head.appendChild(cssTag);
|
||||
},
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import CategoryHashtagType from "discourse/lib/hashtag-types/category";
|
||||
import TagHashtagType from "discourse/lib/hashtag-types/tag";
|
||||
|
||||
export default {
|
||||
name: "register-hashtag-types",
|
||||
before: "hashtag-css-generator",
|
||||
|
||||
initialize() {
|
||||
withPluginApi("0.8.7", (api) => {
|
||||
api.registerHashtagType("category", CategoryHashtagType);
|
||||
api.registerHashtagType("tag", TagHashtagType);
|
||||
});
|
||||
},
|
||||
};
|
@ -14,6 +14,17 @@ import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
let hashtagTypeClasses = {};
|
||||
export function registerHashtagType(type, typeClass) {
|
||||
hashtagTypeClasses[type] = typeClass;
|
||||
}
|
||||
export function cleanUpHashtagTypeClasses() {
|
||||
hashtagTypeClasses = {};
|
||||
}
|
||||
export function getHashtagTypeClasses() {
|
||||
return hashtagTypeClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a textarea using the jQuery autocomplete plugin, specifically
|
||||
* to match on the hashtag (#) character for autocompletion of categories,
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { setOwner } from "@ember/application";
|
||||
|
||||
export default class HashtagTypeBase {
|
||||
constructor(container) {
|
||||
setOwner(this, container);
|
||||
}
|
||||
|
||||
get type() {
|
||||
throw "not implemented";
|
||||
}
|
||||
|
||||
get preloadedData() {
|
||||
throw "not implemented";
|
||||
}
|
||||
|
||||
generateColorCssClasses() {
|
||||
throw "not implemented";
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import HashtagTypeBase from "./base";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class CategoryHashtagType extends HashtagTypeBase {
|
||||
@service site;
|
||||
|
||||
get type() {
|
||||
return "category";
|
||||
}
|
||||
|
||||
get preloadedData() {
|
||||
return this.site.categories;
|
||||
}
|
||||
|
||||
generateColorCssClasses(model) {
|
||||
const generatedCssClasses = [];
|
||||
const backgroundGradient = [`var(--category-${model.id}-color) 50%`];
|
||||
if (model.parentCategory) {
|
||||
backgroundGradient.push(
|
||||
`var(--category-${model.parentCategory.id}-color) 50%`
|
||||
);
|
||||
} else {
|
||||
backgroundGradient.push(`var(--category-${model.id}-color) 50%`);
|
||||
}
|
||||
|
||||
generatedCssClasses.push(`.hashtag-color--category-${model.id} {
|
||||
background: linear-gradient(90deg, ${backgroundGradient.join(", ")});
|
||||
}`);
|
||||
|
||||
return generatedCssClasses;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import HashtagTypeBase from "./base";
|
||||
|
||||
export default class TagHashtagType extends HashtagTypeBase {
|
||||
get type() {
|
||||
return "tag";
|
||||
}
|
||||
|
||||
get preloadedData() {
|
||||
return [];
|
||||
}
|
||||
|
||||
generateColorCssClasses() {
|
||||
return [];
|
||||
}
|
||||
}
|
@ -112,6 +112,7 @@ import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
||||
import { registerModelTransformer } from "discourse/lib/model-transformers";
|
||||
import { registerCustomUserNavMessagesDropdownRow } from "discourse/controllers/user-private-messages";
|
||||
import { registerFullPageSearchType } from "discourse/controllers/full-page-search";
|
||||
import { registerHashtagType } from "discourse/lib/hashtag-autocomplete";
|
||||
|
||||
// If you add any methods to the API ensure you bump up the version number
|
||||
// based on Semantic Versioning 2.0.0. Please update the changelog at
|
||||
@ -2137,6 +2138,17 @@ class PluginApi {
|
||||
addFullPageSearchType(translationKey, searchTypeId, searchFunc) {
|
||||
registerFullPageSearchType(translationKey, searchTypeId, searchFunc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a hastag type and its corresponding class.
|
||||
* This is used when generating CSS classes in the hashtag-css-generator.
|
||||
*
|
||||
* @param {string} type - The type of the hashtag.
|
||||
* @param {Class} typeClass - The class of the hashtag type.
|
||||
*/
|
||||
registerHashtagType(type, typeClass) {
|
||||
registerHashtagType(type, typeClass);
|
||||
}
|
||||
}
|
||||
|
||||
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("CSS Generator", function (needs) {
|
||||
needs.user();
|
||||
needs.site({
|
||||
categories: [
|
||||
{ id: 1, color: "ff0000" },
|
||||
{ id: 2, color: "333" },
|
||||
{ id: 4, color: "2B81AF", parentCategory: { id: 1 } },
|
||||
],
|
||||
});
|
||||
|
||||
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-color--category-1 {\n background: linear-gradient(90deg, var(--category-1-color) 50%, var(--category-1-color) 50%);\n}\n.hashtag-color--category-2 {\n background: linear-gradient(90deg, var(--category-2-color) 50%, var(--category-2-color) 50%);\n}\n.hashtag-color--category-4 {\n background: linear-gradient(90deg, var(--category-4-color) 50%, var(--category-1-color) 50%);\n}"
|
||||
);
|
||||
});
|
||||
});
|
@ -52,6 +52,7 @@ import {
|
||||
cleanUpComposerUploadMarkdownResolver,
|
||||
cleanUpComposerUploadPreProcessor,
|
||||
} from "discourse/components/composer-editor";
|
||||
import { cleanUpHashtagTypeClasses } from "discourse/lib/hashtag-autocomplete";
|
||||
import { resetLastEditNotificationClick } from "discourse/models/post-stream";
|
||||
import { clearAuthMethods } from "discourse/models/login-method";
|
||||
import { clearTopicFooterDropdowns } from "discourse/lib/register-topic-footer-dropdown";
|
||||
@ -190,6 +191,7 @@ export function testCleanup(container, app) {
|
||||
clearTopicFooterDropdowns();
|
||||
clearTopicFooterButtons();
|
||||
clearDesktopNotificationHandlers();
|
||||
cleanUpHashtagTypeClasses();
|
||||
resetLastEditNotificationClick();
|
||||
clearAuthMethods();
|
||||
setTestPresence(true);
|
||||
@ -211,6 +213,12 @@ export function testCleanup(container, app) {
|
||||
resetModelTransformers();
|
||||
resetMentions();
|
||||
cleanupTemporaryModuleRegistrations();
|
||||
cleanupCssGeneratorTags();
|
||||
}
|
||||
|
||||
function cleanupCssGeneratorTags() {
|
||||
document.querySelector("style#category-color-css-generator")?.remove();
|
||||
document.querySelector("style#hashtag-css-generator")?.remove();
|
||||
}
|
||||
|
||||
export function discourseModule(name, options) {
|
||||
|
@ -4,6 +4,7 @@ import { bind } from "discourse-common/utils/decorators";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { MENTION_KEYWORDS } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||
import { clearChatComposerButtons } from "discourse/plugins/chat/discourse/lib/chat-composer-buttons";
|
||||
import ChannelHashtagType from "discourse/plugins/chat/discourse/lib/hashtag-types/channel";
|
||||
import { replaceIcon } from "discourse-common/lib/icon-library";
|
||||
|
||||
let _lastForcedRefreshAt;
|
||||
@ -13,6 +14,7 @@ replaceIcon("d-chat", "comment");
|
||||
|
||||
export default {
|
||||
name: "chat-setup",
|
||||
before: "hashtag-css-generator",
|
||||
|
||||
initialize(container) {
|
||||
this.chatService = container.lookup("service:chat");
|
||||
@ -25,6 +27,8 @@ export default {
|
||||
}
|
||||
|
||||
withPluginApi("0.12.1", (api) => {
|
||||
api.registerHashtagType("channel", ChannelHashtagType);
|
||||
|
||||
api.registerChatComposerButton({
|
||||
id: "chat-upload-btn",
|
||||
icon: "far-image",
|
||||
|
@ -0,0 +1,25 @@
|
||||
import HashtagTypeBase from "discourse/lib/hashtag-types/base";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChannelHashtagType extends HashtagTypeBase {
|
||||
@service chatChannelsManager;
|
||||
@service currentUser;
|
||||
|
||||
get type() {
|
||||
return "channel";
|
||||
}
|
||||
|
||||
get preloadedData() {
|
||||
if (this.currentUser) {
|
||||
return this.chatChannelsManager.publicMessageChannels;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
generateColorCssClasses(model) {
|
||||
return [
|
||||
`.hashtag-color--${this.type}-${model.id} { color: var(--category-${model.chatable.id}-color); }`,
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Chat | Hashtag CSS Generator", function (needs) {
|
||||
const category1 = { id: 1, color: "ff0000" };
|
||||
const category2 = { id: 2, color: "333" };
|
||||
const category3 = { id: 4, color: "2B81AF", parentCategory: { id: 1 } };
|
||||
|
||||
needs.settings({ chat_enabled: true });
|
||||
needs.user({
|
||||
has_chat_enabled: true,
|
||||
chat_channels: {
|
||||
public_channels: [
|
||||
{
|
||||
id: 44,
|
||||
chatable_id: 1,
|
||||
chatable_type: "Category",
|
||||
meta: { message_bus_last_ids: {} },
|
||||
current_user_membership: { following: true },
|
||||
chatable: category1,
|
||||
},
|
||||
{
|
||||
id: 74,
|
||||
chatable_id: 2,
|
||||
chatable_type: "Category",
|
||||
meta: { message_bus_last_ids: {} },
|
||||
current_user_membership: { following: true },
|
||||
chatable: category2,
|
||||
},
|
||||
{
|
||||
id: 88,
|
||||
chatable_id: 4,
|
||||
chatable_type: "Category",
|
||||
meta: { message_bus_last_ids: {} },
|
||||
current_user_membership: { following: true },
|
||||
chatable: category3,
|
||||
},
|
||||
],
|
||||
direct_message_channels: [],
|
||||
meta: { message_bus_last_ids: {} },
|
||||
},
|
||||
});
|
||||
needs.site({
|
||||
categories: [category1, category2, category3],
|
||||
});
|
||||
|
||||
test("hashtag CSS classes are generated", async function (assert) {
|
||||
await visit("/");
|
||||
const cssTag = document.querySelector("style#hashtag-css-generator");
|
||||
assert.equal(
|
||||
cssTag.innerHTML,
|
||||
|
||||
".hashtag-color--category-1 {\n background: linear-gradient(90deg, var(--category-1-color) 50%, var(--category-1-color) 50%);\n}\n.hashtag-color--category-2 {\n background: linear-gradient(90deg, var(--category-2-color) 50%, var(--category-2-color) 50%);\n}\n.hashtag-color--category-4 {\n background: linear-gradient(90deg, var(--category-4-color) 50%, var(--category-1-color) 50%);\n}\n.hashtag-color--channel-44 { color: var(--category-1-color); }\n.hashtag-color--channel-74 { color: var(--category-2-color); }\n.hashtag-color--channel-88 { color: var(--category-4-color); }"
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user