Add custom menu panel to the correct location (same as hamburger menu panel)

In Reuse Discourse Hamburger Functionality, a way to add a new menu item to the header-icons location (i.e. where Discourse hamburger menu is located) is described.

In essence, a call to api.decorateWidget('header-icons:after', callback) is used to (1) add an additional menu entry with its own icon and (2) add a menu panel if its underlying state variable for its visibility is true.

This works, but is slightly inconsistent with the way Discourse’s existing menu entries work: The new menu is added as a child to the header-icons location (i.e. the div.d-header-icons element), while Discourse’s menus are children of div.panel (i.e. the parent element of div.d-header-icons).

This difference causes the added menu to be occluded by the div.header-cloak element for the mobile view.

Is there a way to add a widget to div.panel? Alternatively, is there a chance that the element div.panel will be available as a widget location?

1 Like

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;
}
1 Like