إضافة لوحة القائمة المخصصة إلى الموقع الصحيح (نفس لوحة قائمة الهامبرغر)

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

لقد كنت أحاول حل هذه المشكلة منذ فترة. عنوان قائمة الهامبرغر المخصصة يعرض معلومات اللغة، بحيث عندما أضع المؤشر فوق القائمة، يظهر لي ما يلي:

[en_US.some-title]

عند محاولة استخدام I18n.t(themePrefix("some-title")) بعد إنشاء سمة تحتوي على ملف لغة، يظهر لي ما يلي:

[en_US.Some Title]

كما هو موضح
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: ""
    });
});
السمة مع 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'
    });
});

إليك كود السكربت الكامل الخاص بي. السمة و CSS هما الإعدادات الافتراضية لسمة تم إنشاؤها حديثًا باستخدام discourse_theme new.

ملخص
<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: "Knowledgebase",
            rawTitle: "Knowledgebase",
            href: "/k",
            className: "mysite-about",
            icon: "question"
        }
    ];

    const mySiteSupportLinks = [
        {
            text: "Staff",
            rawTitle: "Staff",
            href: "/page/staff/1/",
            className: "mysite-support",
            icon: "link"
        },
        {
            text: "Contact",
            rawTitle: "Contact",
            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) {
    // This callback has to be provided. Don’t know why.
});

هل لديك أي أفكار حول كيفية تصحيح عنوان التلميح (hover title) ليظهر العنوان فقط دون الأقواس ومعلومات اللغة؟

:قبضة_يد:

لقد تفحصت الأمر وأنا متأكد تماماً أن هذا ليس خطأك، فالرمز البرمجي الذي تستخدمه يجب أن يعمل بشكل صحيح.

المشكلة هنا تكمن في أن سمة العنوان (title attribute) لبرنامج القائمة المنسدلة في الرأس تتوقع مفتاح ترجمة، وليس دالة ترجمة.

لذلك، عندما تستخدم هذا الكود في سمة

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

فإنك في الحقيقة تقوم بتضمين دالتين للترجمة داخل بعضهما البعض.

الدالة الموجودة في سمتك تعيد أي نص قمتَ بتعيينه في ملف اللغة. وفي هذه الحالة هو

عنوان معين

ثم تأخذ الدالة الموجودة في كود البرنامج هذه النصية وتعالجها كمفتاح ترجمة على النحو التالي

I18n.t('عنوان معين')

وهو بالطبع غير موجود، مما يفسر المشكلة التي أبلغتَ عنها، حيث يعيد

[en_US.عنوان معين]

وبعبارة أخرى، هذه عيب في النظام الأساسي. سنقوم بإصلاحه :+1:

هذا جيد أن نعرفه! هل لديك أي أفكار حول سبب عودة الكود كما هو إلى هذه المشكلة أيضًا؟

لقد راجعت السجل وأستطيع القول بثقة معقولة أنه لم تُجرَ أي تغييرات على ذلك الملف بين لحظة نشر المنشور ولحظة طرح سؤالك، من شأنها أن تكون ذات صلة هنا.

أعتقد أنه حتى مع الكود كما هو، كنت ستحصل على

[en_US.custom-menu]

عند تمرير الماوس فوق الرابط إذا أضفت

title: 'custom-menu'

كما هو موضح في الكود الذي قدمه kleinfreund أعلاه.

ربما لم يلاحظوا تلك المشكلة من الأساس.

أيضًا، إذا كنت تريد حلاً مؤقتًا في الوقت الراهن، يمكنك دمج الطريقة القديمة (قبل ترجمات السمات) مع الطريقة الجديدة

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

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

شكرًا لك على الملاحظات، @Johani! وهذا يعمل بشكل رائع، @awesomerobot. أقدر ذلك حقًا! كانت تلك الردود أفضل من أن أرمي رأسي على لوحة المفاتيح أو أن آخذ مسكنًا للألم مثل Motrin!

تحديث: تعطلت الطريقة المذكورة أعلاه مؤخرًا، وأنا أعمل على تحديد ما حدث. إذا كان هناك شخص يعرف منشورًا آخر يغطي أي تغييرات، فسأكون سعيدًا بقراءته.