I found a solution to this issue. The missing piece was the api.addHeaderPanel
method. I’m not quite sure why its third argument, a callback, has to be provided, but passing a do-nothing function works.
Anyway, a theme component with the following files allows one to add a Discourse menu entry and panel in the correct locations that works in both desktop and mobile views.
./common/head_tag.html
:
The following pieces of JavaScript are all part of this script
element:
<script type="text/discourse-plugin" version="0.8">
// …
</script>
First, I’m setting up a data structure that contains the content of the new menu:
const { h } = require('virtual-dom');
const { attachAdditionalPanel } = require('discourse/widgets/header');
const menuItems = [
{
text: 'Home',
href: '/'
},
{
text: 'FAQ',
href: '/faq'
}
];
Second, I’m creating a new widget called custom-menu
. This follows what Discourse uses for its own hamburger menu (see app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6).
api.createWidget('custom-menu', {
tagName: 'div.custom-panel',
settings: {
maxWidth: 320
},
panelContents() {
return h(
'ul.custom-menu',
menuItems.map(item => h('li', h('a', { href: item.href }, item.text)))
);
},
html() {
return this.attach('menu-panel', {
contents: () => this.panelContents(),
maxWidth: this.settings.maxWidth
});
},
clickOutside(event) {
if (this.site.mobileView) {
this.clickOutsideMobile(event);
} else {
this.sendWidgetAction('toggleCustomMenu');
}
},
clickOutsideMobile(event) {
const centeredElement = document.elementFromPoint(event.clientX, event.clientY);
if (
!centeredElement.classList.contains('header-cloak') &&
centeredElement.closest('.panel').length > 0
) {
this.sendWidgetAction('toggleCustomMenu');
} else {
const panel = document.querySelector('.menu-panel');
panel.classList.add('animate');
const panelOffsetDirection = this.site.mobileView ? 'left' : 'right';
panel.style.setProperty(panelOffsetDirection, -window.innerWidth);
const headerCloak = document.querySelector('.header-cloak');
headerCloak.classList.add('animate');
headerCloak.style.setProperty('opacity', 0);
console.log('hi')
Ember.run.later(() => this.sendWidgetAction('toggleCustomMenu'), 200);
}
}
});
Next, I add a new entry to Discourse’s top menu bar (where the search icon, hamburger menu, and the user menu are located). I also setup a widget action that is responsible for changing the state variable that controls the visibility of the custom menu.
api.decorateWidget('header-icons:after', function (helper) {
const headerState = helper.widget.parentWidget.state;
return helper.attach('header-dropdown', {
title: 'custom-menu',
icon: 'bars',
iconId: 'toggle-custom-menu',
active: headerState.customMenuVisible,
action: 'toggleCustomMenu',
});
});
api.attachWidgetAction('header', 'toggleCustomMenu', function() {
this.state.customMenuVisible = !this.state.customMenuVisible;
});
Finally, the following line adds the custom-menu
widget to the correct location in the DOM depending on whether the state variable customMenuVisible
is true.
api.addHeaderPanel('custom-menu', 'customMenuVisible', function (attrs, state) {
// This callback has to be provided. Don’t know why.
});
(Again, all these JavaScript blocks belong into the script
element above.)
./common/common.scss
:
This stylesheets just adds some missing styles that the other panels use.
.d-header .custom-panel {
width: 0;
}
.mobile-view .custom-panel .menu-panel.slide-in {
left: 0;
}