Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev/header and sticky sidebar #18

Merged
merged 14 commits into from
Jun 20, 2024
Merged
17 changes: 17 additions & 0 deletions javascripts/discourse/components/sticky-sidebar.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class="sticky-sidebar">
<div
{{did-insert this.didInsert}}
{{scroll this.onScroll}}
style={{concat

Check failure on line 5 in javascripts/discourse/components/sticky-sidebar.hbs

View workflow job for this annotation

GitHub Actions / ci / linting

Concatenated styles must be marked as `htmlSafe`.
"position: "
this.position
"; top: "
this.top
"px; bottom: "
this.bottom
"px"
}}
>
{{yield}}
</div>
</div>
78 changes: 78 additions & 0 deletions javascripts/discourse/components/sticky-sidebar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";

export default class StickySidebar extends Component {
@tracked top = 0;
@tracked bottom = 0;
@tracked position = "relative";

offset = 0;
prevScrollTop = 0;
yOrigin = 0;
mode = "unset";

@action
onScroll() {
const scrollY = window.scrollY;
const scrollingUp = scrollY < this.prevScrollTop;
const scrollingDown = scrollY > this.prevScrollTop;
const element = this.element;
if (!this.yOrigin) {
// save the initial vertical position
this.yOrigin = getYOrigin(this.element);
}
const stickyTop = this.yOrigin;

if (scrollingUp) {
if (isTopInView(element, this.yOrigin)) {
this.mode = "top";
this.position = "fixed";
this.top = stickyTop;
this.bottom = "unset";
}
if (this.mode === "bottom") {
this.mode = "between";
const top = element.getBoundingClientRect().top;
this.position = "relative";
this.top = top + scrollY - this.yOrigin;
}
} else if (scrollingDown) {
if (isBottomInView(element, this.yOrigin)) {
this.mode = "bottom";
this.position = "fixed";
this.bottom = 0;
this.top = "unset";
}
if (this.mode === "top") {
this.mode = "between";
const top = element.getBoundingClientRect().top;
this.position = "relative";
this.top = top + scrollY - this.yOrigin;
}
}
this.prevScrollTop = scrollY;
}

@action
didInsert(element) {
this.element = element;
this.offset = this.element.offsetTop;
}
}

function isTopInView(element, yOffset) {
const rect = element.getBoundingClientRect();
return rect.top >= yOffset && rect.top <= window.innerHeight;
}

function isBottomInView(element) {
const rect = element.getBoundingClientRect();
return rect.bottom >= 0 && rect.bottom <= window.innerHeight;
}

function getYOrigin(el) {
const rect = el.getBoundingClientRect();
const scrollTop = window.scrollY || window.pageYOffset;
return rect.top + scrollTop;
}
39 changes: 21 additions & 18 deletions javascripts/discourse/connectors/before-main-outlet/blocks.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import BlockProfile from "../../components/blocks/profile";
import BlockTime from "../../components/blocks/time";
import BlockTopContributors from "../../components/blocks/top-contributors";
import BlockTopTopics from "../../components/blocks/top-topics";
import StickySidebarComponent from "../../components/sticky-sidebar";

export default class BlocksComponent extends Component {
@service currentUser;
Expand Down Expand Up @@ -39,24 +40,26 @@ export default class BlocksComponent extends Component {

<template>
{{!log this.blocks}}
<div class="blocks">
<div class="blocks__wrapper">
{{#each this.blocks as |row|}}
<div class="blocks__row">
{{#each row.blocks as |block|}}
{{#let (this.blockify block) as |BlockComponent|}}
{{#if BlockComponent}}
{{component
BlockComponent
size=block.size
period=block.period
}}
{{/if}}
{{/let}}
{{/each}}
</div>
{{/each}}
<StickySidebarComponent>
<div class="blocks">
<div class="blocks__wrapper">
{{#each this.blocks as |row|}}
<div class="blocks__row">
{{#each row.blocks as |block|}}
{{#let (this.blockify block) as |BlockComponent|}}
{{#if BlockComponent}}
{{component
BlockComponent
size=block.size
period=block.period
}}
{{/if}}
{{/let}}
{{/each}}
</div>
{{/each}}
</div>
</div>
</div>
</StickySidebarComponent>
</template>
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,186 @@
import Component from "@glimmer/component";
// import { tracked } from "@glimmer/tracking";
// import { concat, get } from "@ember/helper";
// import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
// import { htmlSafe } from "@ember/template";
// import { eq, or } from "truth-helpers";
// import avatar from "discourse/helpers/avatar";
// import categoryLink from "discourse/helpers/category-link";
// import concatClass from "discourse/helpers/concat-class";
// import number from "discourse/helpers/number";
// import replaceEmoji from "discourse/helpers/replace-emoji";
// import { ajax } from "discourse/lib/ajax";
// import Category from "discourse/models/category";
import { capitalize } from "@ember/string";
import { eq } from "truth-helpers";
import i18n from "discourse-common/helpers/i18n";

export default class Breadcrumbs extends Component {
@service router;
@service site;

@tracked routeType;

@action
updateRouteType() {
if (this.router?.currentRoute?.parent?.name === "discovery") {
switch (this.router?.currentRoute?.localName) {
case "latest":
case "hot":
case "top":
case "new":
case "unread":
this.routeType = "home";
break;
case "category":
case "latestCategory":
case "hotCategory":
case "topCategory":
case "newCategory":
case "unreadCategory":
this.routeType = "category";
break;
case "categories":
this.routeType = "categories";
break;
default:
this.routeType = null;
break;
}
} else {
this.routeType = null;
}
}

get filterType() {
if (this.router?.currentRoute?.localName === "categories") {
return "categories";
}
return this.router?.currentRoute?.attributes?.filterType || "";
}

get isHomepage() {
return this.router.currentRouteName === "discovery.latest";
this.updateRouteType();
return this.routeType === "home";
}

get isCategoryView() {
this.updateRouteType();
return this.routeType === "category";
}

get isCategoryList() {
this.updateRouteType();
return this.routeType === "categories";
}

get categoryName() {
return this.router?.currentRoute?.attributes?.category?.name || "Category";
}

get categoryBadge() {
const defaultBadge = settings.default_category_badge || "📁";

const badge = settings.category_icons?.find(
(category) =>
category.id[0] === this.router?.currentRoute?.attributes?.category?.id
)?.emoji;

if (!badge) {
return defaultBadge;
}

try {
// check for valid emoji
const regex = /\p{Emoji}/u;
if (regex.test(badge)) {
return badge;
}
return defaultBadge;
} catch (e) {
// \p{Emoji} not supported -> skip validation
return badge;
}
}

@action
home() {
this.router.transitionTo("/");
}

<template>
<div class="breadcrumbs">
{{#if this.isHomepage}}
<h2 data-name="home" class="breadcrumbs__title">
<h2 class="breadcrumbs__title">
<div data-badge-type="icon" class="badge">
home
</div>
{{i18n "js.home"}}
</h2>
{{else if this.isCategoryView}}
<h2 class="breadcrumbs__title">
<div
data-badge-type="emoji"
data-clickable="true"
class="badge"
{{on "click" this.home}}

Check failure on line 118 in javascripts/discourse/connectors/discovery-navigation-bar-above/navigation.gjs

View workflow job for this annotation

GitHub Actions / ci / linting

Interaction added to non-interactive element
>
{{this.categoryBadge}}
</div>
{{this.categoryName}}
</h2>
{{else if this.isCategoryList}}
<h2 class="breadcrumbs__title">
<div
data-badge-type="emoji"
data-clickable="true"
class="badge"
{{on "click" this.home}}

Check failure on line 130 in javascripts/discourse/connectors/discovery-navigation-bar-above/navigation.gjs

View workflow job for this annotation

GitHub Actions / ci / linting

Interaction added to non-interactive element
>
🗃️
</div>
{{capitalize (i18n "js.categories.categories_label")}}
</h2>
{{/if}}
<TopicFilter
@filterType={{this.filterType}}
@routeType={{this.routeType}}
/>
</div>
</template>
}
class TopicFilter extends Component {
@service router;
@service site;

@tracked filterOptions;

constructor() {
super(...arguments);

this.filterOptions =
this.site.siteSettings?.top_menu?.split("|").map((item) => {
return { name: item, localization: `js.filters.${item}.title` };
}) || [];
}

@action
filterTopics(event) {
const routeType = this.args.routeType;
const category = this.router?.currentRoute?.attributes?.category;

if (routeType === "category" && event.target.value !== "categories") {
this.router.transitionTo(
`/c/${category.slug}/${category.id}/l/${event.target.value}`
);
} else {
this.router.transitionTo(`/${event.target.value}`);
}
}

<template>
<select
class="breadcrumbs__select"
value={{@filterType}}
onchange={{this.filterTopics}}
>
{{#each this.filterOptions as |filterOption|}}
<option value={{filterOption.name}} selected={{eq filterOption.name @filterType}}>
{{i18n filterOption.localization}}
</option>
{{/each}}
</select>
</template>
}
4 changes: 4 additions & 0 deletions javascripts/discourse/connectors/topic-title/back.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{#unless this.model.isPrivateMessage}}
<a href={{this.model.category.url}} role="button" class="topic-back-button">

Check failure on line 2 in javascripts/discourse/connectors/topic-title/back.hbs

View workflow job for this annotation

GitHub Actions / ci / linting

Links should have descriptive text
</a>
{{/unless}}
11 changes: 11 additions & 0 deletions javascripts/discourse/modifiers/scroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { modifier } from 'ember-modifier';

export default modifier((element, [callback]) => {
const handleScroll = () => callback();

window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
});
Loading
Loading