미디어위키:Common.js

신사 (토론 | 기여)님의 2026년 8월 20일 (목) 15:12 판

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
/* MediaWiki:Common.js */
mw.hook('wikipage.content').add(function ($content) {

    var initialized = {};

    $content.find('[class*="mw-customtoggle-"]').each(function () {
        var classList = (this.className || '').split(/\s+/);

        classList.forEach(function (cls) {
            if (cls.indexOf('mw-customtoggle-') !== 0) {
                return;
            }

            var suffix = cls.substring('mw-customtoggle-'.length);

            if (initialized[suffix]) {
                return;
            }

            initialized[suffix] = true;

            var targetId = 'mw-customcollapsible-' + suffix;

            /*
             * 중복 ID가 있을 수 있으므로 #id 대신 attribute selector 사용
             */
            var $targets = $content.find('[id="' + targetId + '"]');

            if (!$targets.length) {
                return;
            }


            /*
             * =====================================================
             * 특수형
             *
             * 아나운서 표처럼:
             *
             * 상단 버튼 wrapper
             * 실제 내용 패널
             *
             * 이 같은 ID를 공유하고,
             * 실제 패널 안에 wikitable이 있는 구조
             * =====================================================
             */
            var $specialPanel = $targets.filter(function () {
                return $(this).find('table.wikitable').length > 0;
            });

            if ($targets.length > 1 && $specialPanel.length) {
                initStackedCollapsible(
                    $content,
                    cls,
                    $targets
                );

                return;
            }


            /*
             * =====================================================
             * 일반 MediaWiki collapsible
             * =====================================================
             */
            initNormalCollapsible(
                $content,
                cls,
                $targets.first()
            );
        });
    });



    /*
     * =============================================================
     * 일반 펼치기 · 접기
     * =============================================================
     */
    function initNormalCollapsible($content, cls, $target) {

        var $toggles = $content.find('.' + cls);

        /*
         * 일반 MediaWiki 구조:
         *
         * .mw-collapsible
         *   └ .mw-collapsible-content
         *
         * content가 있으면 content만 애니메이션
         */
        var $contentTarget = $target.children('.mw-collapsible-content');

        if (!$contentTarget.length) {
            $contentTarget = $target.find('.mw-collapsible-content').first();
        }

        var $animTarget = $contentTarget.length
            ? $contentTarget
            : $target;


        /*
         * 현재 상태를 기준으로 초기 aria 설정
         */
        var initiallyOpen = $animTarget.is(':visible');

        $toggles.attr(
            'aria-expanded',
            initiallyOpen ? 'true' : 'false'
        );


        $toggles
            .off('click.smoothSlide')
            .on('click.smoothSlide', function (e) {

                e.preventDefault();
                e.stopImmediatePropagation();

                /*
                 * 애니메이션 중 다시 눌렀을 때
                 * queue가 꼬이는 것을 방지
                 */
                if ($animTarget.is(':animated')) {
                    return false;
                }

                var opening = !$animTarget.is(':visible');


                /*
                 * -------------------------------------------------
                 * 펼치기
                 * -------------------------------------------------
                 */
                if (opening) {

                    /*
                     * MediaWiki의 collapsed 상태를 먼저 제거
                     */
                    $target.removeClass('mw-collapsed');


                    /*
                     * slideDown
                     */
                    $animTarget
                        .stop(true, false)
                        .hide()
                        .slideDown(250, function () {

                            /*
                             * jQuery가 남기는 애니메이션용 inline style 정리
                             */
                            $(this).css({
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });
                        });


                    $toggles.attr('aria-expanded', 'true');
                }


                /*
                 * -------------------------------------------------
                 * 접기
                 * -------------------------------------------------
                 */
                else {

                    $animTarget
                        .stop(true, false)
                        .slideUp(250, function () {

                            /*
                             * 애니메이션이 끝난 뒤 collapsed 복원
                             */
                            $target.addClass('mw-collapsed');

                            $(this).css({
                                display: 'none',
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });
                        });


                    $toggles.attr('aria-expanded', 'false');
                }


                return false;
            });
    }



    /*
     * =============================================================
     * 아나운서형 특수 펼치기 · 접기
     * =============================================================
     *
     * DOM 예:
     *
     * [남성 버튼 wrapper]
     * [남성 실제 패널]
     * [여성 버튼 wrapper]
     * [여성 실제 패널]
     *
     * 패널은 원래 DOM 위치에 유지한다.
     *
     * 남성을 펼치면 여성 버튼은
     * 자연스럽게 아래로 밀려난다.
     *
     * 단,
     * 상단 버튼과 패널을 완전히 동시에 움직이지 않고
     * 짧은 시차를 주어 겹침을 방지한다.
     * =============================================================
     */
    function initStackedCollapsible($content, cls, $targets) {

        /*
         * wikitable이 들어 있는 요소를 실제 패널로 사용
         */
        var $panel = $targets.filter(function () {
            return $(this).find('table.wikitable').length > 0;
        }).first();

        if (!$panel.length) {
            return;
        }


        /*
         * 실제 패널이 아닌 동일 ID 요소 중
         * 해당 toggle을 포함하는 요소 = 상단 버튼 wrapper
         */
        var $topWrapper = $targets.not($panel).filter(function () {
            return $(this).find('.' + cls).length > 0;
        }).first();

        if (!$topWrapper.length) {
            return;
        }


        var $topToggle = $topWrapper.find('.' + cls).first();
        var $panelToggle = $panel.find('.' + cls).first();


        if (!$topToggle.length || !$panelToggle.length) {
            return;
        }


        /*
         * =========================================================
         * 애니메이션 설정
         * =========================================================
         *
         * wrapperDuration
         *   상단 버튼이 사라지고 나타나는 속도
         *
         * panelDuration
         *   실제 패널 펼침/접힘 속도
         *
         * openDelay
         *   상단 버튼이 접히기 시작한 뒤
         *   패널이 펼쳐지기 시작하는 시점
         *
         * closeDelay
         *   패널이 접히기 시작한 뒤
         *   상단 버튼이 복귀하기 시작하는 시점
         */
        var wrapperDuration = 180;
        var panelDuration = 240;

        var openDelay = 65;
        var closeDelay = 80;



        /*
         * =========================================================
         * 최초 상태
         * =========================================================
         */
        $panel
            .stop(true, true)
            .addClass('mw-collapsed')
            .hide()
            .css({
                height: '',
                overflow: '',
                paddingTop: '',
                paddingBottom: '',
                marginTop: '',
                marginBottom: '',
                opacity: ''
            });

        $topWrapper
            .stop(true, true)
            .show()
            .css({
                height: '',
                overflow: '',
                paddingTop: '',
                paddingBottom: '',
                marginTop: '',
                marginBottom: '',
                opacity: ''
            });


        $topToggle.attr('aria-expanded', 'false');
        $panelToggle.attr('aria-expanded', 'false');



        /*
         * =========================================================
         * 위쪽 버튼 클릭 → 펼치기
         * =========================================================
         */
        $topToggle
            .off('click.smoothSlide')
            .on('click.smoothSlide', function (e) {

                e.preventDefault();
                e.stopImmediatePropagation();


                /*
                 * 중복 클릭 방지
                 */
                if (
                    $panel.is(':animated') ||
                    $topWrapper.is(':animated') ||
                    $panel.is(':visible')
                ) {
                    return false;
                }


                /*
                 * MediaWiki collapsed 상태 해제
                 */
                $panel.removeClass('mw-collapsed');


                /*
                 * -------------------------------------------------
                 * 1.
                 * 위쪽 버튼 wrapper를 먼저 접기 시작
                 *
                 * 높이뿐 아니라 opacity도 같이 낮춰
                 * 훨씬 자연스럽게 사라지도록 한다.
                 * -------------------------------------------------
                 */
                $topWrapper
                    .stop(true, false)
                    .css({
                        overflow: 'hidden'
                    })
                    .animate(
                        {
                            height: 0,
                            paddingTop: 0,
                            paddingBottom: 0,
                            marginTop: 0,
                            marginBottom: 0,
                            opacity: 0
                        },
                        {
                            duration: wrapperDuration,
                            queue: false,

                            complete: function () {

                                $(this)
                                    .hide()
                                    .css({
                                        height: '',
                                        overflow: '',
                                        paddingTop: '',
                                        paddingBottom: '',
                                        marginTop: '',
                                        marginBottom: '',
                                        opacity: ''
                                    });
                            }
                        }
                    );


                /*
                 * -------------------------------------------------
                 * 2.
                 * 버튼이 어느 정도 접힌 뒤
                 * 실제 패널 펼치기 시작
                 *
                 * 완전 순차식이 아니기 때문에
                 * 움직임이 끊겨 보이지 않는다.
                 * -------------------------------------------------
                 */
                setTimeout(function () {

                    /*
                     * 혹시 다른 처리로 이미 표시되었으면 중단
                     */
                    if ($panel.is(':visible')) {
                        return;
                    }


                    $panel
                        .stop(true, false)
                        .hide()
                        .slideDown(
                            panelDuration,
                            function () {

                                /*
                                 * jQuery animation inline style 정리
                                 */
                                $(this).css({
                                    height: '',
                                    overflow: '',
                                    paddingTop: '',
                                    paddingBottom: '',
                                    marginTop: '',
                                    marginBottom: '',
                                    opacity: ''
                                });
                            }
                        );

                }, openDelay);


                /*
                 * ARIA
                 */
                $topToggle.attr('aria-expanded', 'true');
                $panelToggle.attr('aria-expanded', 'true');


                return false;
            });



        /*
         * =========================================================
         * 패널 안쪽 버튼 클릭 → 접기
         * =========================================================
         */
        $panelToggle
            .off('click.smoothSlide')
            .on('click.smoothSlide', function (e) {

                e.preventDefault();
                e.stopImmediatePropagation();


                /*
                 * 중복 클릭 방지
                 */
                if (
                    $panel.is(':animated') ||
                    $topWrapper.is(':animated')
                ) {
                    return false;
                }


                /*
                 * -------------------------------------------------
                 * 1.
                 * 패널 먼저 접기 시작
                 * -------------------------------------------------
                 */
                $panel
                    .stop(true, false)
                    .slideUp(
                        panelDuration,
                        function () {

                            $(this)
                                .addClass('mw-collapsed')
                                .css({
                                    display: 'none',
                                    height: '',
                                    overflow: '',
                                    paddingTop: '',
                                    paddingBottom: '',
                                    marginTop: '',
                                    marginBottom: '',
                                    opacity: ''
                                });
                        }
                    );


                /*
                 * -------------------------------------------------
                 * 2.
                 * 패널이 어느 정도 접힌 뒤
                 * 원래 버튼을 복귀시키기 시작
                 * -------------------------------------------------
                 */
                setTimeout(function () {

                    /*
                     * 이미 보이는 상태라면 중복 실행하지 않음
                     */
                    if ($topWrapper.is(':visible')) {
                        return;
                    }


                    /*
                     * opacity 0으로 시작
                     */
                    $topWrapper
                        .stop(true, false)
                        .css({
                            opacity: 0
                        })
                        .hide();


                    /*
                     * 높이 복귀
                     */
                    $topWrapper
                        .slideDown({
                            duration: wrapperDuration,

                            step: function () {
                                /*
                                 * slideDown 도중 overflow는
                                 * jQuery가 자동 관리
                                 */
                            },

                            complete: function () {

                                $(this).css({
                                    height: '',
                                    overflow: '',
                                    paddingTop: '',
                                    paddingBottom: '',
                                    marginTop: '',
                                    marginBottom: ''
                                });
                            }
                        });


                    /*
                     * opacity는 slideDown과 동시에 진행
                     *
                     * queue:false이므로
                     * 높이 애니메이션을 기다리지 않는다.
                     */
                    $topWrapper
                        .animate(
                            {
                                opacity: 1
                            },
                            {
                                duration: 130,
                                queue: false,

                                complete: function () {

                                    $(this).css({
                                        opacity: ''
                                    });
                                }
                            }
                        );

                }, closeDelay);


                /*
                 * ARIA
                 */
                $topToggle.attr('aria-expanded', 'false');
                $panelToggle.attr('aria-expanded', 'false');


                return false;
            });
    }

});