Skip to content

Commit 8580e59

Browse files
authored
DEV: Prevent static viewport header link initialization (#48)
This commit updates the header icon link initialization logic to avoid accessing static viewport modes during setup. - Refactors header link rendering to check viewport mode conditionally - Improves reliability by preventing premature or invalid static viewport access
1 parent bff6d88 commit 8580e59

File tree

2 files changed

+90
-67
lines changed

2 files changed

+90
-67
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Component from "@glimmer/component";
2+
import { service } from "@ember/service";
3+
import { htmlSafe } from "@ember/template";
4+
import { eq } from "truth-helpers";
5+
import concatClass from "discourse/helpers/concat-class";
6+
import icon from "discourse/helpers/d-icon";
7+
import dasherize from "discourse/helpers/dasherize";
8+
import { escapeExpression } from "discourse/lib/utilities";
9+
import isValidUrl from "../lib/isValidUrl";
10+
11+
const MOBILE_VIEWS = new Set(["vmo", "vdm"]);
12+
const DESKTOP_VIEWS = new Set(["vdo", "vdm"]);
13+
14+
function shouldRenderViewMode(view, isMobile) {
15+
return isMobile ? MOBILE_VIEWS.has(view) : DESKTOP_VIEWS.has(view);
16+
}
17+
18+
export default class CustomHeaderIcon extends Component {
19+
static shouldRender(args, context) {
20+
return shouldRenderViewMode(args.link.view, context.site.mobileView);
21+
}
22+
23+
@service site;
24+
25+
get iconClassName() {
26+
return `header-icon-${dasherize(this.args.link.title)}`;
27+
}
28+
29+
get isLastLink() {
30+
return this.args.link === this.visibleLinks.at(-1);
31+
}
32+
33+
get style() {
34+
const numericWidth = Number(this.args.link.width);
35+
return Number.isFinite(numericWidth)
36+
? htmlSafe(`width: ${escapeExpression(numericWidth)}px`)
37+
: undefined;
38+
}
39+
40+
get visibleLinks() {
41+
return this.args.links.filter((link) =>
42+
shouldRenderViewMode(link.view, this.site.mobileView)
43+
);
44+
}
45+
46+
<template>
47+
<li
48+
class={{concatClass
49+
"custom-header-icon-link"
50+
this.iconClassName
51+
@link.view
52+
(if this.isLastLink "last-custom-icon")
53+
}}
54+
>
55+
<a
56+
class="btn no-text icon btn-flat"
57+
href={{@link.url}}
58+
title={{@link.title}}
59+
target={{if (eq @link.target "blank") "_blank"}}
60+
rel={{if @link.target "noopener"}}
61+
style={{this.style}}
62+
>
63+
{{#if (isValidUrl @link.icon)}}
64+
<img src={{@link.icon}} aria-hidden="true" />
65+
<span class="sr-only">{{@link.title}}</span>
66+
{{else}}
67+
{{icon @link.icon label=@link.title}}
68+
{{/if}}
69+
</a>
70+
</li>
71+
</template>
72+
}

javascripts/discourse/initializers/initialize-for-header-icon-links.gjs

Lines changed: 18 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,29 @@
1-
import { dasherize } from "@ember/string";
2-
import concatClass from "discourse/helpers/concat-class";
3-
import icon from "discourse/helpers/d-icon";
1+
import curryComponent from "ember-curry-component";
2+
import { getOwnerWithFallback } from "discourse/lib/get-owner";
43
import { withPluginApi } from "discourse/lib/plugin-api";
5-
import { escapeExpression } from "discourse/lib/utilities";
6-
import isValidUrl from "../lib/isValidUrl";
4+
import CustomHeaderIcon from "../components/custom-header-icon";
75

8-
function buildIcon(iconNameOrImageUrl, title) {
9-
if (isValidUrl(iconNameOrImageUrl)) {
10-
return <template>
11-
<img src={{iconNameOrImageUrl}} aria-hidden="true" />
12-
<span class="sr-only">{{title}}</span>
13-
</template>;
14-
} else {
15-
return <template>{{icon iconNameOrImageUrl label=title}}</template>;
16-
}
17-
}
6+
const BEFORE_ICONS = ["chat", "search", "hamburger", "user-menu"];
187

198
export default {
209
name: "header-icon-links",
2110
initialize() {
22-
withPluginApi("0.8.41", (api) => {
11+
withPluginApi((api) => {
2312
try {
24-
const site = api.container.lookup("service:site");
25-
let links = settings.header_links;
26-
if (site.mobileView) {
27-
links = links.filter(
28-
(link) => link.view === "vmo" || link.view === "vdm"
29-
);
30-
} else {
31-
links = links.filter(
32-
(link) => link.view === "vdo" || link.view === "vdm"
33-
);
34-
}
35-
36-
links.forEach((link, index) => {
37-
const iconTemplate = buildIcon(link.icon, link.title);
38-
const className = `header-icon-${dasherize(link.title)}`;
39-
const target = link.target === "blank" ? "_blank" : "";
40-
const rel = link.target ? "noopener" : "";
41-
const isLastLink =
42-
index === links.length - 1 ? "last-custom-icon" : "";
43-
44-
let style = "";
45-
if (link.width) {
46-
style = `width: ${escapeExpression(link.width)}px`;
47-
}
13+
const links = settings.header_links || [];
4814

49-
const iconComponent = <template>
50-
<li
51-
class={{concatClass
52-
"custom-header-icon-link"
53-
className
54-
link.view
55-
isLastLink
56-
}}
57-
>
58-
<a
59-
class="btn no-text icon btn-flat"
60-
href={{link.url}}
61-
title={{link.title}}
62-
target={{target}}
63-
rel={{rel}}
64-
style={{style}}
65-
>
66-
{{iconTemplate}}
67-
</a>
68-
</li>
69-
</template>;
70-
71-
const beforeIcon = ["chat", "search", "hamburger", "user-menu"];
72-
73-
api.headerIcons.add(link.title, iconComponent, {
74-
before: beforeIcon,
75-
});
15+
links.forEach((link) => {
16+
api.headerIcons.add(
17+
link.title,
18+
curryComponent(
19+
CustomHeaderIcon,
20+
{ link, links },
21+
getOwnerWithFallback()
22+
),
23+
{
24+
before: BEFORE_ICONS,
25+
}
26+
);
7627
});
7728
} catch (error) {
7829
// eslint-disable-next-line no-console

0 commit comments

Comments
 (0)