عرض أزرار الملحنين

لقد قمت بعمل نسخة مستنسخة (clone) من هذا المكون الإضافي (TC) في مستودعي على GitHub على الرابط GitHub - denvergeeks/DiscourseComposerButtonBonanza ثم طبقت إصلاحات لهذه الأخطاء [لأن الوصول إلى خدمة Codeberg.org لم يبدُ أنه يعمل للاستنساخ.]

إليك سجل الالتزام (commit log) بتغييراتي:

إذا واجهت أخطاء مع DCBB و/أو لديك إصلاحات لتلك الأخطاء، فسيكون من الرائع أن تتمكن من فتح مشكلة (issue) في المصدر للإبلاغ عن كل هذا، حتى أتمكن من إصلاحها — بدلاً من مجرد استنساخ المستودع (repo) في مكان آخر تمامًا وإخبار الناس باستخدام نسختك المستنسخة. (إذا لم تتمكن من عمل تفرع (fork) للمستودع على Codeberg لسبب ما، فسيكون من الجيد إخباري بذلك مباشرةً أيضًا، بدلاً من إخباري بشكل عَرَضي عبر موضوع نقاش لا أحصل فيه دائمًا على تحديثات البريد الإلكتروني.)

أوه، مرحبًا… بينما أنا على وشك الضغط على Reply، ظهر طلب سحب (PR)! شكرًا لك!

هل يعالج طلب السحب هذا أيًا من المشاكل التي تحدث مع <div> مقابل <span> تحت محرر WYSIWYG-Composer (ما أرهق @shauny) — أم أنه يعالج خطأ/خلل آخر، أم أنه يعالج تحذيرات الإهمال فقط (أي الأشياء التي ستتوقف عن العمل في أي يوم، لكنها ليست معطلة حتى الآن)؟

أرغب في فهم ما تغير خلف الكواليس. أي إشارات إلى مواضيع أو وثائق Discourse ستكون محل تقدير كبير.

لقد عملت مع ChatGPT لإنشاء حل يمكنني إدراجه مباشرة في ملف JS الخاص بي.

ما يفعله هذا هو اعتراض زر “Spoiler” (الغامض) ثم تشغيل الزر الافتراضي بدلاً منه.

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

إليك الكود الذي أضفته إلى ملف JS الخاص بالسمة (theme) الخاصة بي:

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
  /**
   * === إصلاح الغموض لمحرر النص المنسق (RTE) لزر Composer Bonanza ===
   *
   * سطح المكتب: يعمل عن طريق النقر على الخيارات (Options)، ثم النقر على "تمويه الغموض" (Blur spoiler).
   * الهاتف المحمول: تختلف علامات القائمة المنسدلة، لذلك يجب علينا تحديد موقع العنصر حسب النص، وليس المحددات الصارمة.
   *
   */

  const BONANZA_SPOILER_SELECTOR = "button.ComposerButtonBonanza-btn-spoiler";

  const OPTIONS_TRIGGER_SELECTORS = [
    'button[aria-label="Options"]',
    'button[title="Options"]',
    'button[aria-label*="options" i]',
    'button[title*="options" i]',
  ].join(",");

  function isRichTextComposerContext(el) {
    const composer = el.closest(".d-editor, .composer, .composer-controls");
    if (!composer) return false;

    const hasProseMirror = !!composer.querySelector(".ProseMirror");
    const hasTextarea = !!composer.querySelector("textarea");
    return hasProseMirror && !hasTextarea;
  }

  function getComposerRoot(el) {
    return el.closest(".d-editor, .composer, .composer-controls") || document;
  }

  function normalizeText(s) {
    return (s || "").trim().replace(/\s+/g, " ");
  }

  function isVisible(el) {
    if (!el) return false;
    const style = window.getComputedStyle(el);
    if (!style) return false;
    if (style.display === "none" || style.visibility === "hidden") return false;
    const r = el.getBoundingClientRect?.();
    return !!r && r.width > 0 && r.height > 0;
  }

  function openOptionsMenu(composerRoot) {
    const btn = composerRoot.querySelector(OPTIONS_TRIGGER_SELECTORS);
    if (!btn) return false;
    btn.click();
    return true;
  }

  function findOpenDropdownMenuRoot() {
    // في Discourse، تستخدم القوائم المنسدلة عادةً أحد هذه الحاويات.
    // نختار الأخير المرئي (الأحدث فتحاً).
    const candidates = [
      ...document.querySelectorAll(".dropdown-menu, .dropdown-menu__items, .dropdown-menu__content"),
    ].filter(isVisible);

    return candidates.length ? candidates[candidates.length - 1] : null;
  }

  function findBlurSpoilerButton(menuRoot) {
    if (!menuRoot) return null;

    // أولاً، نجرب بنية سطح المكتب (المسار السريع)
    const exact =
      menuRoot.querySelector('button[title="Blur spoiler"]') ||
      menuRoot.querySelector('button[aria-label="Blur spoiler"]');
    if (exact) return exact;

    // وإلا، نحدد الموقع حسب محتوى النص في أي مكان داخل عناصر القائمة المنسدلة
    const itemRoots = Array.from(
      menuRoot.querySelectorAll(".dropdown-menu__item, li, div, button")
    );

    for (const node of itemRoots) {
      const text = normalizeText(node.textContent).toLowerCase();
      if (text === "blur spoiler" || text.includes("blur spoiler")) {
        // ننقر على الزر الأقرب الفعلي
        const btn = node.closest("button") || node.querySelector?.("button");
        if (btn) return btn;
        // إذا كان بالفعل عنصراً يشبه الزر، نعيده
        if (node.tagName === "BUTTON") return node;
      }
    }

    // الملاذ الأخير: مسح جميع الأزرار في القائمة حسب نص التسمية
    const buttons = Array.from(menuRoot.querySelectorAll("button"));
    for (const btn of buttons) {
      const t = normalizeText(btn.textContent).toLowerCase();
      const title = normalizeText(btn.getAttribute("title") || "").toLowerCase();
      const aria = normalizeText(btn.getAttribute("aria-label") || "").toLowerCase();

      if (t.includes("blur spoiler") || title === "blur spoiler" || aria === "blur spoiler") {
        return btn;
      }
    }

    return null;
  }

  function clickBlurSpoilerFromOpenMenu() {
    const menuRoot = findOpenDropdownMenuRoot();
    const btn = findBlurSpoilerButton(menuRoot);
    if (!btn) return false;

    // تتطلب بعض القوائم المنسدلة للهاتف المحمول نقرة لإنهاء تحديد الموضع/إرفاق المعالجات
    requestAnimationFrame(() => {
      requestAnimationFrame(() => btn.click());
    });

    return true;
  }

  function openOptionsAndClickBlurSpoiler(composerRoot) {
    if (!openOptionsMenu(composerRoot)) return false;

    // إعادة المحاولة حتى وجود القائمة والعنصر
    let tries = 0;
    const maxTries = 30;

    const attempt = () => {
      tries++;
      if (clickBlurSpoilerFromOpenMenu()) return;
      if (tries < maxTries) setTimeout(attempt, 35);
    };

    setTimeout(attempt, 0);
    return true;
  }

  document.addEventListener(
    "click",
    (e) => {
      const bonanzaBtn = e.target.closest(BONANZA_SPOILER_SELECTOR);
      if (!bonanzaBtn) return;

      // نتجاوز فقط في محرر النص المنسق (RTE)؛ وضع ماركداون (markdown) نترك Bonanza وشأنه.
      if (!isRichTextComposerContext(bonanzaBtn)) return;

      // منع إدراج كود BBCode الخاص بـ Bonanza في محرر النص المنسق
      e.preventDefault();
      e.stopImmediatePropagation();

      const composerRoot = getComposerRoot(bonanzaBtn);
      openOptionsAndClickBlurSpoiler(composerRoot);
    },
    true
  );
});

إنه حل غير تقليدي، ولكنه يعمل. تم اختباره في نظامي التشغيل iOS/macOS Safari و macOS Chrome فقط، ولم يتم اختباره على نظام Android بعد.