Benutzerdefiniertes Menüpanel an der richtigen Stelle hinzufügen (wie beim Hamburger-Menü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?

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

Ich habe mich schon eine Weile den Kopf darüber zerbrochen. Der Titel des benutzerdefinierten Hamburger-Menüs zeigt die Lokalisierungsinformationen an, sodass beim Hover über das Menü Folgendes ausgegeben wird:

[en_US.some-title]

Wenn ich versuche, I18n.t(themePrefix("some-title")) zu verwenden, nachdem ich ein Theme mit einer Lokalisierungsdatei erstellt habe, erhalte ich Folgendes:

[en_US.Some Title]

Wie angegeben
api.decorateWidget('header-icons:before', function (helper) {
    const headerState = helper.widget.parentWidget.state;

    return helper.attach('header-dropdown', {
      title: "mysite.menu_title",
      icon: 'bars',
      iconId: 'toggle-custom-menu',
      active: headerState.customMenuVisible,
      action: 'toggleCustomMenu',
      href: ""
    });
});
Theme mit I18n
api.decorateWidget('header-icons:before', function (helper) {
    const headerState = helper.widget.parentWidget.state;

    return helper.attach('header-dropdown', {
      title: I18n.t(themePrefix("mysite.menu_title")),
      icon: 'bars',
      iconId: 'toggle-custom-menu',
      active: headerState.customMenuVisible,
      action: 'toggleCustomMenu'
    });
});

Hier ist mein vollständiger Skriptcode. Das Theme und das CSS sind die Standardwerte für ein neu generiertes Theme, das mit discourse_theme new erstellt wurde.

Zusammenfassung
<script type="text/discourse-plugin" version="0.8">

const { h } = require('virtual-dom');
const { attachAdditionalPanel } = require('discourse/widgets/header');
const { iconNode } = require("discourse-common/lib/icon-library");

const mySiteAboutLinks = [
        {
            text: "Wissensdatenbank",
            rawTitle: "Wissensdatenbank",
            href: "/k",
            className: "mysite-about",
            icon: "question"
        }
    ];

    const mySiteSupportLinks = [
        {
            text: "Mitarbeiter",
            rawTitle: "Mitarbeiter",
            href: "/page/staff/1/",
            className: "mysite-support",
            icon: "link"
        },
        {
            text: "Kontakt",
            rawTitle: "Kontakt",
            href: "/page/contact/2/",
            className: "mysite-support",
            icon: "far-envelope"
        }
    ];

api.createWidget('custom-menu', {
    tagName: 'div.custom-panel',
    
    settings: {
        maxWidth: 320
    },
    
    panelContents() {
        results = [];
        
        results[1] = h('ul.custom-about-menu.menu-links.columned', mySiteAboutLinks.map(l => h('li', h('a.widget-link', { href: l.href, title: l.rawTitle }, [ iconNode(l.icon.toLowerCase()), ' ', h('span.d-label', l.text) ]))));
        results[2] = h('div.clearfix');
        results[3] = h('hr');
        results[4] = h('ul.custom-support-menu.menu-links.columned', mySiteSupportLinks.map(k => h('li', h('a.widget-link', { href: k.href, title: k.rawTitle }, [ iconNode(k.icon.toLowerCase()), ' ', h('span.d-label', k.text) ]))))
        
        return results;
        
    },

    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);
        }}
});

api.decorateWidget('header-icons:before', function (helper) {
    const headerState = helper.widget.parentWidget.state;

    return helper.attach('header-dropdown', {
      title: I18n.t(themePrefix("mysite.menu_title")),
      icon: 'bars',
      iconId: 'toggle-custom-menu',
      active: headerState.customMenuVisible,
      action: 'toggleCustomMenu'
    });
});


api.attachWidgetAction('header', 'toggleCustomMenu', function() {
    this.state.customMenuVisible = !this.state.customMenuVisible;
});

api.addHeaderPanel('custom-menu', 'customMenuVisible', function (attrs, state) {
    // Dieser Callback muss bereitgestellt werden. Weiß nicht warum.
});

Hast du eine Idee, wie ich den Hover-Titel korrigieren kann, sodass nur der Titel ohne Klammern und Lokalisierung angezeigt wird?

:fist:

Ich habe mir das angesehen und bin mir ziemlich sicher, dass dies nicht deine Schuld ist. Der von dir verwendete Code sollte funktionieren.

Das Problem hier ist, dass das Attribut title für das Dropdown-Widget der Kopfzeile einen Übersetzungsschlüssel und keine Übersetzungsfunktion erwartet.

Wenn du dies also in einem Theme verwendest:

title: I18n.t(themePrefix("mysite.menu_title"))

verschachtelst du im Grunde zwei Übersetzungsfunktionen.

Die eine in deinem Theme gibt den String zurück, den du in der Lokalisierungsdatei festgelegt hast. In diesem Fall ist es:

Some title

Die andere im Widget-Code nimmt diesen String dann und verarbeitet ihn als Übersetzungsschlüssel wie folgt:

I18n.t('Some title')

was offensichtlich nicht existiert. Daher tritt das von dir gemeldete Problem auf, bei dem es zurückgibt:

[en_US.Some Title]

Mit anderen Worten: Dies ist ein Fehler im Kerncode. Wir werden ihn beheben :+1:

Das ist gut zu wissen! Hast du eine Idee, warum der angegebene Code trotzdem ein solches Problem verursacht?

Ich habe mir die Historie angesehen und kann mit einer gewissen Sicherheit sagen, dass zwischen dem Zeitpunkt, als der Beitrag erstellt wurde, und dem Zeitpunkt, an dem Sie Ihre Frage stellten, keine für diesen Fall relevanten Änderungen an dieser Datei vorgenommen wurden.

Meine Vermutung ist, dass Sie selbst mit dem aktuellen Code beim Überfahren des Links mit der Maus

[en_US.custom-menu]

erhalten hätten, wenn Sie

title: 'custom-menu'

hinzugefügt hätten, wie von kleinfreund im oben bereitgestellten Code angegeben.

Vielleicht ist ihnen dieses Problem einfach nie aufgefallen.

Falls Sie vorübergehend eine Workaround-Lösung benötigen, können Sie die alte Methode (vor den Theme-Übersetzungen) mit der neuen Methode kombinieren:

I18n.translations.en.js.custom_translation = I18n.t(themePrefix("custom_theme_translation"));

return helper.attach('header-dropdown', {
  title: 'custom_translation',

Danke für das Feedback, @Johani! Und das funktioniert wunderbar, @awesomerobot. Ich schätze das! Diese Antworten haben besser funktioniert, als wenn ich mir den Kopf auf die Tastatur geschlagen und/oder Motrin genommen hätte!

Update: Der oben beschriebene Ansatz ist kürzlich nicht mehr funktioniert, und ich arbeite daran, herauszufinden, was passiert ist. Falls jemand einen anderen Beitrag kennt, der Änderungen behandelt, würde ich ihn gerne lesen.