إظهار مكون العلامة في قوائم المواضيع - توسيع/طي العلامات في قوائم المواضيع

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

:warning: إفصاح: تم تخطيط مكون السمة هذا وتنفيذه واختباره بمساعدة أدوات الترميز بالذكاء الاصطناعي.

أود أن أسمع ملاحظاتك!


:information_source: ملخص كشف العلامات
: eyeglasses: معاينة غير متوفر…
:hammer_and_wrench: المستودع GitHub - jrgong420/discourse-tag-reveal
:question: دليل التثبيت كيفية تثبيت سمة أو مكون سمة
:open_book: جديد في سمات Discourse؟ دليل المبتدئين لاستخدام سمات Discourse

Discourse Tag Reveal هو مكون سمة خفيف الوزن يحافظ على قوائم المواضيع مرتبة من خلال عرض أول N علامة لكل موضوع فقط واستبدال الباقي بعلامة تبويب قابلة للوصول “+X علامات إضافية”. يمكن للمستخدمين التوسع لرؤية جميع العلامات والطي مرة أخرى إلى العرض المختصر. إنه يعمل مباشرة مع واجهة العلامات القياسية في Discourse ولا يتطلب أي تغييرات من جانب الخادم.

الميزات

  • حد علامات قابل للتكوين (الافتراضي: 5) عبر إعدادات السمة

  • علامة تبويب مصممة كعلامة، يمكن الوصول إليها عبر لوحة المفاتيح (Enter/Space) مع سمات ARIA

  • سلاسل نصية مترجمة باستخدام themePrefix و discourse-i18n

  • سلوك آمن للتطبيقات أحادية الصفحة (SPA): يعيد تعيين المنطق ويعيد تطبيقه عند تغيير الصفحات

  • يدعم التمرير اللانهائي عبر MutationObserver

  • حد أدنى من CSS؛ يحترم أنماط العلامات الأساسية

  • لا يتطلب تجاوزات للقوالب أو تبعيات الإضافات

لقطات الشاشة / العرض التجريبي

…قريبًا

التثبيت والتكوين

  • تم الاختبار مع إصدار Discourse: 3.6.0beta1

  • قم بتكوين الإعدادات ضمن علامة تبويب الإعدادات الخاصة بالمكون:

  • max_tags_visible (عدد صحيح، الافتراضي 5): عدد العلامات التي سيتم عرضها قبل الطي

  • toggle_tag_style: النمط المرئي لعلامة التبديل لمطابقة مظهر العلامة (حاليًا تم تنفيذ نمط “box” فقط)

  • النطاق: يؤثر على قوائم المواضيع (أحدث، جديدة، غير مقروءة، وقوائم مواضيع الفئات)

التوافق مع مكونات السمات الأخرى

:warning: تم إجراء اختبارات بسيطة فقط، يرجى الاختبار بنفسك قبل النشر في بيئة الإنتاج

ملاحظات

  • تأكد من تمكين العلامات (المسؤول → الإعدادات → العلامات)، وإلا فلن ترى أي تأثير

  • إذا كان موقعك يخصص CSS للعلامات بشكل كبير، فقد ترغب في تعديل أنماط .ts-toggle لمحاذاة بصرية مثالية

أفكار للمستقبل

لا أخطط حقًا لتنفيذ المزيد من الميزات ولكني سعيد بقبول طلبات السحب. بعض الأفكار للمستقبل:

  • تمكين/تعطيل العلامات في عرض الموضوع

  • تحكم دقيق للصفحات و/أو الفئات المحددة

إعجابَين (2)

هل قمت بتسمية الإعداد عن قصد بنفس اسم إعداد في النواة؟ سأكون قلقًا بشأن سوء الفهم.

إعجاب واحد (1)

صيد جيد! لقد قمت بتعديله للتو…

إعجابَين (2)

يبدو مثيراً للاهتمام حقاً. سأجربه على بيئة التطوير الخاصة بي لاحقاً بما أنه لا يبدو أنه يعمل على Theme Creator (إلا إذا كنت أفعل شيئاً خاطئاً؟) :thinking:.

تبدو مثيرة للاهتمام! هل يمكنك مشاركة بعض لقطات الشاشة أو التسجيلات للشاشة للميزة أثناء عملها؟

:smiley: تمت إضافة عرض فيديو سريع في المنشور الأول، انظر هنا:

لم أتحقق حتى من كيفية تقديم/إضافة مكون TC الخاص بي هناك…:smiley:
ولكن على أي حال، أفضل جمع بعض التعليقات هنا أولاً، وبمجرد أن يكون جاهزاً للنشر في #theme-component، سأرى كيفية إضافته هناك.

3 إعجابات

منشئ السمات لا يستخدم نمط الصندوق

قد ترغب في استخدام

more_tags:
  one: "+%{count} علامة أخرى"
  other: "+%{count} علامات أخرى"
إعجاب واحد (1)

نقطة جيدة. لقد نسيت تغيير التسمية الافتراضية إلى +%{count} المزيد للحفاظ عليها قصيرة وموجزة، هكذا نستخدمها ونحافظ على الأشياء مدمجة ونظيفة.

إعجاب واحد (1)

أهلاً،

هذه الميزة قد تكون مثيرة للاهتمام في بعض المواقف!

للوهلة الأولى، هناك بعض الأشياء التي يجب ملاحظتها:

  • إعدادات المظهر وإعدادات الموقع ليستا متماثلتين. تحتاج إلى استرداد الخدمة أولاً للوصول إلى max_tags_per_topic، على سبيل المثال: const siteSettings = api.container.lookup(\"service:site-settings\");

  • يجب ألا تكون الفحوصات الإضافية للحصول على الحد ضرورية؛ يمكنك استرداد القيمة مباشرة. يمكنك على الأرجح القيام بـ Math.min(settings.max_tags_visible, siteSettings.max_tags_per_topic )

  • أنت لا تستعيد رؤية الفواصل.

  • قد ترغب في إلغاء تسجيل الأحداث

  • لا ينبغي أن تكون العملية عند التحميل الأولي ضرورية مع MutationObserver. عادةً، قبل الانتقال إلى النطاق العام، سترغب في التحقق أولاً مما إذا كانت هناك طريقة لتقليل النطاق حول العنصر باستخدام واجهة برمجة التطبيقات (مخرج المكون الإضافي، على سبيل المثال).

سأتحقق مما إذا كانت هناك طريقة مختلفة!

إعجاب واحد (1)

بما أن هذا الملف موجود في ملف api-initializers، فهل سيعمل @service siteSettings أيضًا؟

هل يمكنك التحقق الآن؟ يجب أن يكون آخر تثبيت قد أصلح النقاط التي تم تناولها

إصدار Discourse الأدنى 3.6.0 يعني أن الأمر سيستغرق وقتًا طويلاً حتى يتمكن أي شخص من استخدامه. هل قصدت 3.5.0 أو 3.6.0beta1؟

كنت أعني 3.6.0beta1، هذا هو الإصدار الذي كنت أختبره به…

أنت تستخدم هذا في فئة. لن يعمل بخلاف ذلك.

تريد كتابة 3.6.0.beta1 بعد ذلك، وإلا فلن يتمكن أحد من تثبيته في الوقت الحالي.

لقد تحققت قليلاً. بالفعل، لا توجد طريقة مباشرة لتحقيق ذلك؛ ومع ذلك، وجدت طريقة مثيرة للاهتمام ومبسطة للقيام بذلك باستخدام واجهة برمجة التطبيقات (API).

  • يستخدم نموذج الموضوع لتغيير العلامات المرئية التي سيتم إخراجها قبل إنشاء القالب. هذا يعني عدم وجود معالجة DOM وإعدادات مستقلة. اعتمادًا على الحالة (revealTags)، سيعيد القائمة الأصلية أو قائمة جزئية.

  • لإنشاء زر التبديل، يستخدم واجهة برمجة التطبيقات (API) لإضافة علامة مع HTML لزر (للأسف، لا يوجد منفذ إضافي للمكون الإضافي هنا). يتم التعامل مع حدث النقر بشكل منفصل. عند النقر، يتم تحديث حالة التبديل (revealTags)، ونقوم بتشغيل إعادة عرض قائمة العلامات.

الميزة الكبيرة لهذه الطريقة هي أنك لا تضطر إلى العبث بـ HTML ومعرفة ما يجب إظهاره/إلغاء إخفائه باستخدام CSS، بناءً على الأساليب المختلفة.

chrome_lSKqwYt5Z7

أشارك رمز الاختبار الخاص بي هنا:

import { apiInitializer } from "discourse/lib/api";
import { i18n } from "discourse-i18n";
import { computed } from "@ember/object";

export default apiInitializer((api) => {
  const siteSettings = api.container.lookup("service:site-settings");

  const maxVisibleTags = Math.min(
    settings.max_tags_visible,
    siteSettings.max_tags_per_topic
  );

  let topicModels = {};

  api.modifyClass(
    "model:topic",
    (Superclass) =>
      class extends Superclass {
        revealTags = false;

        init() {
          super.init(...arguments);
          topicModels[this.id] = this;
        }

        @computed("tags")
        get visibleListTags() {
          if (this.revealTags) {
            return super.visibleListTags;
          }
          return super.visibleListTags.slice(0, maxVisibleTags);
        }
      }
  );

  api.addTagsHtmlCallback(
    (topic, params) => {
      if (topic.tags.length <= maxVisibleTags) {
        return "";
      }

      const isExpanded = topic.revealTags;
      const label = isExpanded
        ? i18n(themePrefix("js.tag_reveal.hide"))
        : i18n(themePrefix("js.tag_reveal.more_tags"), {
            count: topic.tags.length - maxVisibleTags,
          });

      return `<a class="reveal-tag-action" role="button" aria-expanded="${isExpanded}">${label}</a>`;
    },
    {
      priority: siteSettings.max_tags_per_topic + 1,
    }
  );

  document.addEventListener("click", (event) => {
    const target = event.target;
    if (!target?.matches(".reveal-tag-action")) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    const element =
      target.closest("[data-topic-id]") ||
      document.querySelector("h1[data-topic-id]");
    const topicId = element?.dataset.topicId;
    if (!topicId) {
      return;
    }

    const topicModel = topicModels[topicId];
    if (!topicModel) {
      return;
    }

    topicModel.revealTags = !topicModel.revealTags;
    topicModel.notifyPropertyChange("tags");
  });
});
.reveal-tag-action {
  background-color: var(--primary-50);
  border: 1px solid var(--primary-200);
  color: var(--primary-800);
  font-size: small;
  padding-inline: 3px;
}

.discourse-tags__tag-separator:has(+ .reveal-tag-action) {
  visibility: hidden;
}

إعجابَين (2)

مرحباً يا رفاق، لقد دفعت تحديثًا آخر وأضفت ميزات تجريبية إضافية (“علامات مميزة” تأتي دائمًا أولاً ولا تُحسب ضمن الحد الأقصى + صف موضوع مميز في عرض قائمة الموضوعات) لذا فإن TC الإجمالي يتحول قليلاً بوظائف موسعة لتسليط الضوء على أشياء معينة بناءً على علامات التبويب المكونة.

@Arkshine شكرًا لمشاركة طريقتك المبسطة، أقدر ذلك حقًا!!! لقد أثر ذلك أيضًا على عرض الموضوع الفردي، لذا أضفنا إعدادًا لتمكين هذا السلوك يدويًا. لقد قمت بتطبيقه في هذا الفرع:

  • أعتقد أنك بحاجة إلى مراجعة CSS.

    • ربما لا يجب عليك إضافة discourse-tag إلى زر التبديل، فهو ليس علامة.
    • لا تستخدم فئة box عليه أيضًا، فهو يفسد نمط القائمة
    • إعداد toggle_tag_style يحتوي فقط على القيمة “box”، ربما يمكنك إضافة “none”، حتى يتناسب بشكل أفضل مع نمط القائمة/النقاط.
    • ابدأ ببساطة ويمكنك التعديل كما تريد
      .reveal-tag-action {
        color: var(--primary-500);
      
        &.-box {
          background-color: var(--primary-50);
          outline: 1px solid var(--primary-200);
          padding-inline: 8px;
        }
      }
      
      /* يخفي الفاصل الأخير قبل زر التبديل */
      .discourse-tags__tag-separator:has(+ .reveal-tag-action) {
        visibility: hidden;
      }
      

    Bs1rdLFyIU

    نمط الصندوق في إعدادات الموقع والمظهر:
    chrome_raXs2Gc1sd

    سأقدم المزيد من الملاحظات حول CSS الذي تقوم بحقنه. حان وقت النوم الآن.

إعجاب واحد (1)

في الواقع، كان ذلك مقصودًا. على سبيل المثال، يستخدم المكون الإضافي للتصويت على المواضيع الفئة على عناصر “x votes”.

→ شاهده أثناء العمل في فئة Feature - Discourse Meta

شكرًا، سأتحقق!

حتى الآن، قمت بتطبيق نمط الصندوق فقط لأنه ما أستخدمه في مثيل Discourse الخاص بنا. سأضيف الأنماط المفقودة لاحقًا (مفتوح لطلبات السحب :wink: )

أفهم. أعتقد أن الأمر منطقي إذا عرضت المعلومات كعلامة، ولكن هنا، إنها زر لعرض المزيد من العلامات؛ السياق مختلف بالنسبة لي. الأمر متروك لك؛ لا أعتقد أن الأمر مهم كثيرًا.

للمتابعة بالتعليقات:

  • يمكن عرض قائمة العلامات في أماكن أخرى، مثل: صفحة الفئات، أنشطة المستخدم، إلخ. قد أقوم بإزالة إعداد collapse_in_topic_view وإنشاء إعداد جديد بمسارات محددة أو تمكينه في كل مكان.
    في رمز الاختبار الخاص بي، استخدمت شيئًا كهذا لتجاهل المسارات الأخرى:
JS
    function isAllowedRoute(routeName) {
        const fullRoutesName = [
          "index",
          "userActivity.topics",
          "userActivity.read",
          ...siteSettings.top_menu.split("|").map((item) => `discovery.${item}`),
        ];

        const partialRoutesName = ["topic."];

        if (
          fullRoutesName.includes(routeName) ||
          partialRoutesName.some((partial) => routeName.startsWith(partial))
        ) {
          return true;
        }

        return false;
      }
  • يمكن استبدال حقن CSS باستخدام واجهة برمجة التطبيقات (API) لإضافة فئة إلى topic-list-item وإلى علامة، ثم نقل CSS إلى common.css.

على سبيل المثال:

JS
```js
import { defaultRenderTag } from "discourse/lib/render-tag";

api.registerValueTransformer(
  "topic-list-item-class",
  ({ value, context }) => {
    if (highlightedTagsSet.size === 0) {
      return value;
    }

    if (context.topic?.tags?.some((tag) => highlightedTagsSet.has(tag))) {
      return [...value, `highlighted-tag__${settings.highlighted_style}`];
    }

    return value;
  }
);

api.replaceTagRenderer((tag, params) => {
  if (highlightedTagsSet.has(tag)) {
    params.extraClass = params.extraClass || "";
    params.extraClass += "highlighted";
  }

  return defaultRenderTag(tag, params);
});
```
CSS
/* Hides the last separator before the toggle button */
.discourse-tags__tag-separator:has(+ .reveal-tag-action) {
  visibility: hidden;
}

.reveal-tag-action {
  color: var(--primary-500);

  &.-box {
    background-color: var(--primary-50);
    outline: 1px solid var(--primary-200);
    padding-inline: 8px;
  }
}

.latest-topic-list-item,
.topic-list-item {
  .discourse-tag.highlighted {
    color: var(--tertiary);
    border-color: var(--tertiary);
    background: color-mix(in srgb, var(--tertiary) 12%, transparent);
    font-weight: 600;
  }

  &.highlighted-tag {
    &__left-border {
      border-left: 3px solid var(--tertiary);
      background: color-mix(in srgb, var(--tertiary) 6%, transparent);
      transition: box-shadow 160ms ease;

      &:hover {
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
      }
    }

    &__outline {
      outline: 1px solid var(--tertiary);
      outline-offset: -2px;
      border-radius: 7px;
      background: color-mix(in srgb, var(--tertiary) 5%, transparent);
      box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06);
      transition: background-color 160ms ease;
    }

    &__card {
      border-left: 3px solid var(--tertiary);
      background: var(--tertiary-very-low);
      border-radius: var(--border-radius);
      padding-block: var(--space-2);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
      transition: box-shadow 160ms ease;

      &:hover {
        box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1);
      }
    }
  }
}

  ```
  • لست بحاجة إلى تعيين المسار الحالي من onPageChange، يمكنك الوصول إليه من الموجه (router).
  • كن حذرًا بشأن حالة أحرف العلامات. لديك إعدادات موقع لا تفرض الأحرف الصغيرة، لذلك أعتقد أنه من الأفضل عدم تعديل العلامة.
  • حول إعادة تعيين الحالة، يمكنك على الأرجح استخدام onPageChange.
JS
```js
api.onPageChange((url) => {
    const route = api.container.lookup("service:router").recognize(url);
    if (!isAllowedRoute(route?.name)) {
      return;
    }

    for (const [id, model] of topicModels) {
      if (model && model.revealTags) {
        model.revealTags = false;
        model.notifyPropertyChange("tags");
      }
    }
  });
```
  • إذا استطعت، سيكون من الرائع إضافة اختبارات.

إليك رمز الاختبار الكامل (لقد أجريت تغييرات طفيفة أخرى)

JS
import { computed } from "@ember/object";
import { apiInitializer } from "discourse/lib/api";
import { i18n } from "discourse-i18n";
import { defaultRenderTag } from "discourse/lib/render-tag";
import { service } from "@ember/service";

export default apiInitializer((api) => {
  const siteSettings = api.container.lookup("service:site-settings");
  const router = api.container.lookup("service:router");

  const maxVisibleTags = Math.min(
    settings.max_tags_visible,
    siteSettings.max_tags_per_topic
  );

  const highlightedTagsSet = new Set(settings.highlighted_tags.split("|"));
  const topicModels = new Map();

  function isAllowedRoute(routeName) {
    const fullRoutesName = [
      "index",
      "userActivity.topics",
      "userActivity.read",
      "tag.show",
      ...siteSettings.top_menu.split("|").map((item) => `discovery.${item}`),
    ];

    const partialRoutesName = ["topic."];

    if (
      fullRoutesName.includes(routeName) ||
      partialRoutesName.some((partial) => routeName.startsWith(partial))
    ) {
      return true;
    }

    return false;
  }

  api.modifyClass(
    "model:topic",
    (Superclass) =>
      class extends Superclass {
        @service router;

        revealTags = false;

        init() {
          super.init(...arguments);
          topicModels.set(String(this.id), this);
        }

        willDestroy() {
          super.willDestroy(...arguments);
          topicModels.delete(String(this.id));
        }

        @computed("tags")
        get visibleListTags() {
          const baseTags = super.visibleListTags || [];

          if (!isAllowedRoute(this.router.currentRouteName)) {
            return baseTags;
          }

          const highlightedList = [];
          const regularList = [];

          baseTags.forEach((tag) => {
            if (highlightedTagsSet.has(tag)) {
              highlightedList.push(tag);
            } else {
              regularList.push(tag);
            }
          });

          if (this.revealTags) {
            return [...highlightedList, ...regularList];
          }

          return [...highlightedList, ...regularList.slice(0, maxVisibleTags)];
        }
      }
  );

  api.addTagsHtmlCallback(
    (topic) => {
      if (!isAllowedRoute(topic.router.currentRouteName)) {
        return "";
      }

      const allTags = topic.tags || [];
      if (allTags.length === 0) {
        return "";
      }

      const highlightedCount = allTags.filter((tag) =>
        highlightedTagsSet.has(tag)
      ).length;
      const regularCount = allTags.length - highlightedCount;
      const effectiveLimit =
        highlightedCount + Math.min(regularCount, maxVisibleTags);

      // Only show toggle if there are hidden tags
      if (allTags.length <= effectiveLimit) {
        return "";
      }

      const isExpanded = topic.revealTags;
      const hiddenCount = allTags.length - effectiveLimit;
      const label = isExpanded
        ? i18n(themePrefix("js.tag_reveal.hide"))
        : i18n(themePrefix("js.tag_reveal.more_tags"), {
            count: hiddenCount,
          });

      const classList = ["discourse-tag", "reveal-tag-action"];
      if (settings.toggle_tag_style === "box") {
        classList.push("-box");
      }

      return `<a class="${classList.join(" ")}" role="button" aria-expanded="${isExpanded}">${label}</a>`;
    },
    {
      priority: siteSettings.max_tags_per_topic + 1,
    }
  );

  api.registerValueTransformer(
    "topic-list-item-class",
    ({ value, context }) => {
      if (highlightedTagsSet.size === 0) {
        return value;
      }

      if (context.topic?.tags?.some((tag) => highlightedTagsSet.has(tag))) {
        return [...value, `highlighted-tag__${settings.highlighted_style}`];
      }

      return value;
    }
  );

  api.replaceTagRenderer((tag, params) => {
    let newParams = params;

    if (highlightedTagsSet.has(tag)) {
      newParams = {
        ...params,
        extraClass: [params.extraClass, "highlighted"]
          .filter(Boolean)
          .join(" "),
      };
    }

    return defaultRenderTag(tag, newParams);
  });

  document.addEventListener(
    "click",
    (event) => {
      const target = event.target;
      if (!target?.matches(".reveal-tag-action")) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();

      const element =
        target.closest("[data-topic-id]") ||
        document.querySelector("h1[data-topic-id]");
      const topicId = element?.dataset.topicId;
      if (!topicId) {
        return;
      }

      const topicModel = topicModels.get(topicId);
      if (!topicModel) {
        return;
      }

      topicModel.revealTags = !topicModel.revealTags;
      topicModel.notifyPropertyChange("tags");
    },
    true
  );

  api.onPageChange((url) => {
    const route = api.container.lookup("service:router").recognize(url);
    if (!isAllowedRoute(route?.name)) {
      return;
    }

    for (const [id, model] of topicModels) {
      if (model && model.revealTags) {
        model.revealTags = false;
        model.notifyPropertyChange("tags");
      }
    }
  });
});