DEV: Auto expand active sections and scroll active link into view
This commit is contained in:
parent
11369018b6
commit
b7cce1a0dc
|
@ -11,7 +11,11 @@ export default class SidebarApiPanels extends Component {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-sections {{this.panelCssClass}}">
|
<div class="sidebar-sections {{this.panelCssClass}}">
|
||||||
<ApiSections @collapsable={{@collapsableSections}} />
|
<ApiSections
|
||||||
|
@collapsable={{@collapsableSections}}
|
||||||
|
@expandActiveSection={{this.sidebarState.currentPanel.expandActiveSection}}
|
||||||
|
@scrollActiveLinkIntoView={{this.sidebarState.currentPanel.scrollActiveLinkIntoView}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { and, eq } from "truth-helpers";
|
||||||
import Section from "./section";
|
import Section from "./section";
|
||||||
import SectionLink from "./section-link";
|
import SectionLink from "./section-link";
|
||||||
|
|
||||||
|
@ -14,6 +15,9 @@ const SidebarApiSection = <template>
|
||||||
@displaySection={{@section.displaySection}}
|
@displaySection={{@section.displaySection}}
|
||||||
@hideSectionHeader={{@section.hideSectionHeader}}
|
@hideSectionHeader={{@section.hideSectionHeader}}
|
||||||
@collapsedByDefault={{@section.collapsedByDefault}}
|
@collapsedByDefault={{@section.collapsedByDefault}}
|
||||||
|
@activeLink={{@section.activeLink}}
|
||||||
|
@expandWhenActive={{@expandWhenActive}}
|
||||||
|
@scrollActiveLinkIntoView={{@scrollActiveLinkIntoView}}
|
||||||
>
|
>
|
||||||
{{#each @section.filteredLinks key="name" as |link|}}
|
{{#each @section.filteredLinks key="name" as |link|}}
|
||||||
<SectionLink
|
<SectionLink
|
||||||
|
@ -23,6 +27,7 @@ const SidebarApiSection = <template>
|
||||||
@model={{link.model}}
|
@model={{link.model}}
|
||||||
@query={{link.query}}
|
@query={{link.query}}
|
||||||
@models={{link.models}}
|
@models={{link.models}}
|
||||||
|
@currentWhen={{link.currentWhen}}
|
||||||
@href={{link.href}}
|
@href={{link.href}}
|
||||||
@title={{link.title}}
|
@title={{link.title}}
|
||||||
@contentCSSClass={{link.contentCSSClass}}
|
@contentCSSClass={{link.contentCSSClass}}
|
||||||
|
@ -38,7 +43,6 @@ const SidebarApiSection = <template>
|
||||||
@hoverValue={{link.hoverValue}}
|
@hoverValue={{link.hoverValue}}
|
||||||
@hoverAction={{link.hoverAction}}
|
@hoverAction={{link.hoverAction}}
|
||||||
@hoverTitle={{link.hoverTitle}}
|
@hoverTitle={{link.hoverTitle}}
|
||||||
@currentWhen={{link.currentWhen}}
|
|
||||||
@didInsert={{link.didInsert}}
|
@didInsert={{link.didInsert}}
|
||||||
@willDestroy={{link.willDestroy}}
|
@willDestroy={{link.willDestroy}}
|
||||||
@content={{link.text}}
|
@content={{link.text}}
|
||||||
|
@ -46,6 +50,10 @@ const SidebarApiSection = <template>
|
||||||
link.contentComponent
|
link.contentComponent
|
||||||
status=link.contentComponentArgs
|
status=link.contentComponentArgs
|
||||||
}}
|
}}
|
||||||
|
@scrollIntoView={{and
|
||||||
|
@scrollActiveLinkIntoView
|
||||||
|
(eq link.name @section.activeLink.name)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ApiSection from "./api-section";
|
||||||
import PanelHeader from "./panel-header";
|
import PanelHeader from "./panel-header";
|
||||||
|
|
||||||
export default class SidebarApiSections extends Component {
|
export default class SidebarApiSections extends Component {
|
||||||
|
@service router;
|
||||||
@service sidebarState;
|
@service sidebarState;
|
||||||
|
|
||||||
get sections() {
|
get sections() {
|
||||||
|
@ -20,7 +21,7 @@ export default class SidebarApiSections extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return sectionConfigs.map((Section) => {
|
return sectionConfigs.map((Section) => {
|
||||||
const SidebarSection = prepareSidebarSectionClass(Section);
|
const SidebarSection = prepareSidebarSectionClass(Section, this.router);
|
||||||
|
|
||||||
const sectionInstance = new SidebarSection({
|
const sectionInstance = new SidebarSection({
|
||||||
filterable:
|
filterable:
|
||||||
|
@ -43,14 +44,19 @@ export default class SidebarApiSections extends Component {
|
||||||
<PanelHeader @sections={{this.filteredSections}} />
|
<PanelHeader @sections={{this.filteredSections}} />
|
||||||
|
|
||||||
{{#each this.filteredSections as |section|}}
|
{{#each this.filteredSections as |section|}}
|
||||||
<ApiSection @section={{section}} @collapsable={{@collapsable}} />
|
<ApiSection
|
||||||
|
@section={{section}}
|
||||||
|
@collapsable={{@collapsable}}
|
||||||
|
@expandWhenActive={{@expandActiveSection}}
|
||||||
|
@scrollActiveLinkIntoView={{@scrollActiveLinkIntoView}}
|
||||||
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|
||||||
// extends the class provided for the section to add functionality we don't want to be overridable when defining custom
|
// extends the class provided for the section to add functionality we don't want to be overridable when defining custom
|
||||||
// sections using the plugin API, like for example the filtering capabilities
|
// sections using the plugin API, like for example the filtering capabilities
|
||||||
function prepareSidebarSectionClass(Section) {
|
function prepareSidebarSectionClass(Section, routerService) {
|
||||||
return class extends Section {
|
return class extends Section {
|
||||||
constructor({ filterable, sidebarState }) {
|
constructor({ filterable, sidebarState }) {
|
||||||
super();
|
super();
|
||||||
|
@ -82,6 +88,46 @@ function prepareSidebarSectionClass(Section) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get activeLink() {
|
||||||
|
return this.filteredLinks.find((link) => {
|
||||||
|
try {
|
||||||
|
const currentWhen = link.currentWhen;
|
||||||
|
|
||||||
|
if (typeof currentWhen === "boolean") {
|
||||||
|
return currentWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO detect active links using the href field
|
||||||
|
|
||||||
|
const queryParams = link.queryParams || {};
|
||||||
|
let models;
|
||||||
|
|
||||||
|
if (link.model) {
|
||||||
|
models = [link.model];
|
||||||
|
} else if (link.models) {
|
||||||
|
models = link.models;
|
||||||
|
} else {
|
||||||
|
models = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof currentWhen === "string") {
|
||||||
|
return currentWhen.split(" ").some((route) =>
|
||||||
|
routerService.isActive(route, ...models, {
|
||||||
|
queryParams,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return routerService.isActive(link.route, ...models, {
|
||||||
|
queryParams,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// false if ember throws an exception while checking the routes
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get filtered() {
|
get filtered() {
|
||||||
return !this.filterable || this.filteredLinks?.length > 0;
|
return !this.filterable || this.filteredLinks?.length > 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { hash } from "@ember/helper";
|
import { hash } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
import { on } from "@ember/modifier";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
|
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||||
import { LinkTo } from "@ember/routing";
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import { schedule } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { eq, or } from "truth-helpers";
|
import { eq, or } from "truth-helpers";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import icon from "discourse-common/helpers/d-icon";
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
import deprecated from "discourse-common/lib/deprecated";
|
import deprecated from "discourse-common/lib/deprecated";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import SectionLinkPrefix from "./section-link-prefix";
|
import SectionLinkPrefix from "./section-link-prefix";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,6 +65,14 @@ export default class SectionLink extends Component {
|
||||||
classNames.push(this.args.class);
|
classNames.push(this.args.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.args.href &&
|
||||||
|
typeof this.args.currentWhen === "boolean" &&
|
||||||
|
this.args.currentWhen
|
||||||
|
) {
|
||||||
|
classNames.push("active");
|
||||||
|
}
|
||||||
|
|
||||||
return classNames.join(" ");
|
return classNames.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,9 +113,22 @@ export default class SectionLink extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
scrollIntoView(element) {
|
||||||
|
if (this.args.scrollIntoView) {
|
||||||
|
schedule("afterRender", () => {
|
||||||
|
element.scrollIntoView({
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.shouldDisplay}}
|
{{#if this.shouldDisplay}}
|
||||||
<li
|
<li
|
||||||
|
{{didInsert this.scrollIntoView}}
|
||||||
|
{{didUpdate this.scrollIntoView @scrollIntoView}}
|
||||||
data-list-item-name={{@linkName}}
|
data-list-item-name={{@linkName}}
|
||||||
class="sidebar-section-link-wrapper"
|
class="sidebar-section-link-wrapper"
|
||||||
...attributes
|
...attributes
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { hash } from "@ember/helper";
|
import { hash } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
import { on } from "@ember/modifier";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
@ -19,15 +20,27 @@ import SectionHeader from "./section-header";
|
||||||
|
|
||||||
export default class SidebarSection extends Component {
|
export default class SidebarSection extends Component {
|
||||||
@service keyValueStore;
|
@service keyValueStore;
|
||||||
|
@service router;
|
||||||
@service sidebarState;
|
@service sidebarState;
|
||||||
|
|
||||||
|
@tracked activeExpanded = false;
|
||||||
|
|
||||||
sidebarSectionContentId = getSidebarSectionContentId(this.args.sectionName);
|
sidebarSectionContentId = getSidebarSectionContentId(this.args.sectionName);
|
||||||
collapsedSidebarSectionKey = getCollapsedSidebarSectionKey(
|
collapsedSidebarSectionKey = getCollapsedSidebarSectionKey(
|
||||||
this.args.sectionName
|
this.args.sectionName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.router.on("routeDidChange", this, this.expandIfActive);
|
||||||
|
}
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
super.willDestroy(...arguments);
|
super.willDestroy(...arguments);
|
||||||
|
|
||||||
|
this.router.off("routeDidChange", this, this.expandIfActive);
|
||||||
|
|
||||||
this.args.willDestroy?.();
|
this.args.willDestroy?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,12 +60,20 @@ export default class SidebarSection extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isActive() {
|
||||||
|
return !!this.args.activeLink;
|
||||||
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
setExpandedState() {
|
setExpandedState() {
|
||||||
if (!isEmpty(this.sidebarState.filter)) {
|
if (!isEmpty(this.sidebarState.filter)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.expandIfActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isCollapsed) {
|
if (this.isCollapsed) {
|
||||||
this.sidebarState.collapseSection(this.args.sectionName);
|
this.sidebarState.collapseSection(this.args.sectionName);
|
||||||
} else {
|
} else {
|
||||||
|
@ -60,11 +81,26 @@ export default class SidebarSection extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
expandIfActive(transition) {
|
||||||
|
if (transition?.isAborted) {
|
||||||
|
return this.activeExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeExpanded = this.args.expandWhenActive && this.isActive;
|
||||||
|
|
||||||
|
return this.activeExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
get displaySectionContent() {
|
get displaySectionContent() {
|
||||||
if (this.args.hideSectionHeader || !isEmpty(this.sidebarState.filter)) {
|
if (this.args.hideSectionHeader || !isEmpty(this.sidebarState.filter)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.activeExpanded) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return !this.sidebarState.collapsedSections.has(
|
return !this.sidebarState.collapsedSections.has(
|
||||||
this.collapsedSidebarSectionKey
|
this.collapsedSidebarSectionKey
|
||||||
);
|
);
|
||||||
|
@ -72,6 +108,8 @@ export default class SidebarSection extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleSectionDisplay(_, event) {
|
toggleSectionDisplay(_, event) {
|
||||||
|
this.activeExpanded = false;
|
||||||
|
|
||||||
if (this.displaySectionContent) {
|
if (this.displaySectionContent) {
|
||||||
this.sidebarState.collapseSection(this.args.sectionName);
|
this.sidebarState.collapseSection(this.args.sectionName);
|
||||||
} else {
|
} else {
|
||||||
|
@ -134,6 +172,7 @@ export default class SidebarSection extends Component {
|
||||||
@sidebarSectionContentId={{this.sidebarSectionContentId}}
|
@sidebarSectionContentId={{this.sidebarSectionContentId}}
|
||||||
@toggleSectionDisplay={{this.toggleSectionDisplay}}
|
@toggleSectionDisplay={{this.toggleSectionDisplay}}
|
||||||
@isExpanded={{this.displaySectionContent}}
|
@isExpanded={{this.displaySectionContent}}
|
||||||
|
@isActive={{this.isActive}}
|
||||||
>
|
>
|
||||||
{{#if @collapsable}}
|
{{#if @collapsable}}
|
||||||
<span class="sidebar-section-header-caret">
|
<span class="sidebar-section-header-caret">
|
||||||
|
|
|
@ -57,6 +57,14 @@ export default class BaseCustomSidebarPanel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get expandActiveSection() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get scrollActiveLinkIntoView() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} filter filter applied
|
* @param {string} filter filter applied
|
||||||
*
|
*
|
||||||
|
|
|
@ -36,6 +36,11 @@ export default class BaseCustomSidebarSectionLink {
|
||||||
*/
|
*/
|
||||||
get models() {}
|
get models() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} `query` argument for the <LinkTo> component. See See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo.
|
||||||
|
*/
|
||||||
|
get query() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} `current-when` argument for the <LinkTo> component. See See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo.
|
* @returns {boolean} `current-when` argument for the <LinkTo> component. See See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue