작곡가 버튼 대박

ChatGPT와 함께 제 JS 파일에 그대로 삽입할 수 있는 수정 코드를 만들었습니다.

이 코드는 “Spoiler” 버튼을 가로채고 대신 기본 버튼을 트리거합니다.

다른 방법이 없다면 이 기능이 플러그인의 일부로 포함되었으면 좋겠습니다(이deal적으로는 메뉴를 호출하고 버튼을 클릭하는 대신, 버튼이 트리거하는 동일한 동작을 직접 실행하는 것이 좋습니다).

제 테마의 JS 파일에 추가한 코드입니다:

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

export default apiInitializer((api) => {
  /**
   * === Composer Button Bonanza용 RTE Spoiler 수정 ===
   *
   * 데스크톱: 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에서만 오버라이드합니다. 마크다운 모드에서는 Bonanza를 그대로 둡니다.
      if (!isRichTextComposerContext(bonanzaBtn)) return;

      // RTE에서 Bonanza의 BBCode 삽입을 방지합니다
      e.preventDefault();
      e.stopImmediatePropagation();

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

해킹적인 방식이지만, 작동합니다. iOS/macOS Safari와 macOS Chrome에서만 테스트했으며, Android에서는 아직 테스트하지 않았습니다.

1개의 좋아요