إذا واجهت أخطاء مع 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 بعد.
لقد أصدرت للتو الإصدار 2.0.0 من Composer Button Bonanza. التغيير الوحيد هو إصلاح الاستخدام المهمل لـ site.desktopView. راجع التزام المصدر للحصول على التفاصيل.
لقد قمت بتحديث ذلك التحذير. بعد التحقيق في المشكلات الآن، أعتقد أن الأمر معكوس في الواقع: محرر النص المنسق غير متوافق مع هذا المكون الإضافي الخاص بالمظهر، لأن تنفيذه لـ ProsemirrorTextManipulation لواجهة TextManipulation غير مكتمل و/أو غير صحيح.
على وجه الخصوص:
تنفيذ ProsemirrorTextManipulation.applyList() لا يستخدم تمامًا المعامل head الذي يوفره المتصل. بدلاً من ذلك، يبحث عن المفتاح الخاص بـ نص المثال الذي يوفره المتصل لتخمين ما يحاول المتصل القيام به، وهو مبرمج بشكل ثابت ليفهم فقط الأزرار المضمنة للقوائم النقطية، والقوائم المرقمة، والاقتباسات الكتلية.
تنفيذ ProsemirrorTextManipulation.applySurround() لا يتطابق مع سلوك تنفيذ TextareaTextManipulation.applySurround() الأصلي، وهو المسؤول عن استخدام <div> بشكل عشوائي حتى عندما يجب أن يستخدم <span>. يتجاهل تنفيذ Prosemirror أيضًا المعامل opts لـ applySurround(). (وكذلك، باستخدام نفس الحيلة مثل applyList(), فإنه يبرمج بشكل ثابت مفاتيح نص المثال للكشف عن أزرار المائل، والغامق، والنص المنسق مسبقًا.)
@renato، هل هذه المشكلات على رادار أي شخص؟ هل هناك جدول زمني لإصلاحها؟
بعض واجهات برمجة التطبيقات (APIs) التي تم إنشاؤها عندما كان هناك محرر منطقة نصية فقط، ليست مُعدة حقًا لتحقيق تكافؤ كامل مع المحرر الغني، وليس هدفنا هو جلب كل قوة ProseMirror إلى تجريد وسيط.
يمكننا تحسين تلك الأماكن إذا أمكن ولزم الأمر، ولكن بشكل عام عندما نحتاج إلى عمليات معقدة، فإننا نصل عادةً إلى تبعيات ProseMirror مباشرةً من خلال مفتاح commands على امتداد محرر غني مُسجل. على سبيل المثال:
في هذا المثال، يقوم applySurround بتطبيق bbcode الخاص بالمانع الأذى (spoiler) بشكل أعمى على أي نص محدد، بينما يحتوي toggleSpoiler على جميع ميزات ProseMirror ليقرر ما إذا كان بالفعل داخل عقدة مانع أذى (spoiler node)، وما إذا كان مانع أذى مضمّن (inline spoiler) أو مانع أذى كتلة (block spoiler)، إلخ.
إذا تم تنفيذ هاتين الطريقتين بدقة أكبر للواجهة التي تنتميان إليها، أعتقد أن Composer Button Bonanza سيعمل “بمجرد” في المحرر الغني الجديد (على الأقل بقدر ما يعمل في وضع ترميز Markdown). أنا مندهش من أن مؤلفي مكونات السمات أو الإضافات الآخرين لم يثيروا أي مشاكل ذات صلة حتى الآن. (على الرغم من أنهم ربما فعلوا؛ لم أحاول البحث عن شكاوى مماثلة.)
لا أعرف ما الذي تتضمنه “كل قوة ProseMirror”، لكني أشك في أن ذلك ضروري. واجهات applyList() و applySurround() ليست معقدة للغاية - على الرغم من أنه يوجد بها أكثر مما تم تنفيذه حتى الآن.
(ما تم تنفيذه حتى الآن لا يبدو أنه “تطبيق ترميز القائمة على التحديد” أو “تطبيق ترميز النص الذي يحيط بالتحديد” قائم على مبادئ، بل هو أشبه بـ “مجرد إرسال الاستدعاءات من أزرار الشريط الأدوات المضمنة والمعروفة إلى وظائف prosemirror محددة”.)
كنتُ أعمل على نسختي الخاصة من مكون موضوع مشابه، يسمح بإضافة اختصارات لوحة مفاتيح مخصصة لعمليات التغليف المخصصة (سواء من نوع [wrap] أو أي محتوى HTML/ماركداون/خاص بـ Discourse مسموح به).
على سبيل المثال، يمكنني تعريف Ctrl+Shift+K لإدراج أو تغليف التحديد بـ <kbd></kbd>، أو <hr>، أو [wrap=announcement][/wrap] (سواء مضمن أو كتلة)، أو أي شيء نريده، كما أضفتُ أزرارًا اختيارية في قائمة الترس.
يدعم الأسطر الجديدة باستخدام \n في المحتوى الذي ندرجه.
كما أنه متوافق مع محرر النصوص الغنية. إليك الكود للرجوع إليه: https://github.com/Canapin/discourse-custom-shortcuts، مع تحذير: تم كتابته بمساعدة Claude ولم أراجع الكود بعد. بالإضافة إلى ذلك، لو كنتُ سأستمر في صنع حل خاص بي بدلاً من استخدام حلّك، فسأقوم على الأرجح بتغيير أو إضافة ميزات لاستخدام بعض ميزاتك.
لنعد إذن إلى مكونك، فأنا أحب تنوعه، لكنه يفتقر إلى شيئين أساسيين بالنسبة لي:
دعم محرر النصوص الغنية
طريقة سهلة لإضافة محتوى مخصص (تغليف مخصص، وسوم، أو غيرها)
في حين يمكنني برمجة أعمالي الخاصة باستخدام الذكاء الاصطناعي، إلا أنني لستُ مرتاحًا بما يكفي للاعتماد على الذكاء الاصطناعي لاقتراح طلبات دمج (مثل دعم محرر النصوص الغنية) لمكونك، كما أنني لستُ مبرمجًا.
بصرف النظر عن مسألة محرر النصوص الغنية، ما رأيك في جدوى إضافة محتوى مخصص جديد من خلال مكونك (بشكل أساسي ما وصفته عن مكوني الخاص)؟ هل سيُقبل أم أنه خارج نطاقه؟