미디어위키:Common.js: 두 판 사이의 차이

편집 요약 없음
편집 요약 없음
 
(같은 사용자의 중간 판 35개는 보이지 않습니다)
1번째 줄: 1번째 줄:
/**
/* MediaWiki:Common.js */
* MediaWiki 커스텀 펼치기/접기 애니메이션
mw.hook('wikipage.content').add(function ($content) {
*
* 지원:
*
* .mw-customtoggle-이름
*          ↕
* .mw-customcollapsible-이름
*
* 예:
*
* .mw-customtoggle-KSC_LEADER
* .mw-customcollapsible-KSC_LEADER
*/


( function () {
    var initialized = {};
'use strict';


var DURATION = 300;
    $content.find('[class*="mw-customtoggle-"]').each(function () {
var EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
        var classList = (this.className || '').split(/\s+/);


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


var INITIALIZED_CLASS =
            var suffix = cls.substring('mw-customtoggle-'.length);
'mw-custom-collapse-initialized';


            if (initialized[suffix]) {
                return;
            }


/**
            initialized[suffix] = true;
* 토글의 식별자 추출
*
* mw-customtoggle-KSC_LEADER
* → KSC_LEADER
*/
function getToggleName( toggle ) {
var prefix = 'mw-customtoggle-';
var classes = toggle.classList;
var i;


for ( i = 0; i < classes.length; i++ ) {
            var targetId = 'mw-customcollapsible-' + suffix;


if (
            /*
classes[ i ].indexOf( prefix ) === 0
            * 중복 ID가 있을 수 있으므로
) {
            * #id 대신 attribute selector 사용
return classes[ i ].substring(
            */
prefix.length
            var $targets = $content.find('[id="' + targetId + '"]');
);
}
}


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




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


if (
            if ($targets.length > 1 && $specialPanel.length) {
window.CSS &&
                initStackedCollapsible(
typeof window.CSS.escape === 'function'
                    $content,
) {
                    cls,
return window.CSS.escape( value );
                    $targets
}
                );


return value.replace(
                return;
/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g,
            }
'\\$1'
);
}




/**
            /*
* 토글 이름에 대응하는 대상 찾기
            * =====================================================
*/
            * 일반 MediaWiki collapsible
function getTarget( name ) {
            * =====================================================
            */
            initNormalCollapsible(
                $content,
                cls,
                $targets.first()
            );
        });
    });


if ( !name ) {
return null;
}


var selector =
'.mw-customcollapsible-' +
escapeSelector( name );


var target =
    /*
document.querySelector( selector );
    * =============================================================
    * 일반 펼치기 · 접기
    * =============================================================
    */
    function initNormalCollapsible($content, cls, $target) {


if ( target ) {
        var $toggles = $content.find('.' + cls);
return target;
}


return document.getElementById(
        /*
'mw-customcollapsible-' + name
        * 일반 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;
function getContent( target ) {


var content = target.querySelector(
':scope > .mw-collapsible-content'
);


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


content = target.querySelector(
        $toggles.attr(
'.mw-collapsible-content'
            'aria-expanded',
);
            initiallyOpen ? 'true' : 'false'
        );


if ( content ) {
return content;
}


return target;
        $toggles
}
            .off('click.smoothSlide')
            .on('click.smoothSlide', function (e) {


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


/**
* 요소의 자연스러운 display 값
*/
function getNaturalDisplay( element ) {


switch ( element.tagName ) {
                /*
                * 애니메이션 중 재클릭 방지
                */
                if ($animTarget.is(':animated')) {
                    return false;
                }


case 'TABLE':
return 'table';


case 'TBODY':
                var opening = !$animTarget.is(':visible');
return 'table-row-group';


case 'THEAD':
return 'table-header-group';


case 'TFOOT':
                /*
return 'table-footer-group';
                * -------------------------------------------------
                * 펼치기
                * -------------------------------------------------
                */
                if (opening) {


case 'TR':
                    /*
return 'table-row';
                    * MediaWiki collapsed 상태 제거
                    */
                    $target.removeClass('mw-collapsed');


case 'TD':
case 'TH':
return 'table-cell';


case 'COLGROUP':
                    /*
return 'table-column-group';
                    * 내용만 펼침
                    */
                    $animTarget
                        .stop(true, true)
                        .hide()
                        .slideDown(250, function () {


case 'COL':
                            /*
return 'table-column';
                            * jQuery 애니메이션 inline style 정리
                            */
                            $(this).css({
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });
                        });


case 'CAPTION':
return 'table-caption';


case 'LI':
                    $toggles.attr('aria-expanded', 'true');
return 'list-item';
                }


default:
return 'block';
}
}


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


/**
                    $animTarget
* 현재 접힌 상태인지 확인
                        .stop(true, true)
*/
                        .slideUp(250, function () {
function isCollapsed( target, content ) {


if (
                            /*
target.classList.contains(
                            * 애니메이션 종료 후 collapsed 복원
'mw-collapsed'
                            */
)
                            $target.addClass('mw-collapsed');
) {
return true;
}


var style =
                            $(this).css({
window.getComputedStyle( content );
                                display: 'none',
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });
                        });


return (
style.display === 'none' ||
content.hidden === true
);
}


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


/**
* 현재 실행 중인 애니메이션 취소
*/
function cancelCurrentAnimation( content ) {


if ( content._mwCollapseAnimation ) {
                return false;
            });
    }


try {
content._mwCollapseAnimation.cancel();
} catch ( e ) {
// 무시
}


content._mwCollapseAnimation = null;
}
}


    /*
    * =============================================================
    * 아나운서형 특수 펼치기 · 접기
    * =============================================================
    *
    * DOM 예:
    *
    * [남성 버튼 wrapper]
    * [남성 실제 패널]
    * [여성 버튼 wrapper]
    * [여성 실제 패널]
    *
    * 패널을 이동시키지 않고
    * 원래 DOM 위치에서 펼친다.
    *
    * 중요한 점:
    *
    * wrapper와 panel을 둘 다 애니메이션하지 않는다.
    *
    * 버튼 wrapper는 즉시 교체하고,
    * 실제 패널 하나만 slide 애니메이션한다.
    *
    * 따라서 이중 움직임이나
    * 중간 겹침 현상이 최소화된다.
    * =============================================================
    */
    function initStackedCollapsible($content, cls, $targets) {


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


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


content.classList.add(
ANIMATING_CLASS
);


/*
        /*
* MediaWiki의 접힘 클래스 먼저 제거
        * 실제 패널이 아닌 동일 ID 요소 중
*/
        * 해당 toggle을 포함하는 요소 = 상단 버튼 wrapper
target.classList.remove(
        */
'mw-collapsed'
        var $topWrapper = $targets.not($panel).filter(function () {
);
            return $(this).find('.' + cls).length > 0;
        }).first();


content.hidden = false;
        if (!$topWrapper.length) {
            return;
        }




/*
        var $topToggle = $topWrapper.find('.' + cls).first();
* display:none 해제
        var $panelToggle = $panel.find('.' + cls).first();
*/
content.style.display =
getNaturalDisplay( content );


content.style.overflow = 'hidden';


content.style.height = 'auto';
        if (!$topToggle.length || !$panelToggle.length) {
            return;
        }




/*
* 실제 최종 높이 계산
*/
var targetHeight =
content.scrollHeight;


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


/*
* 애니메이션 시작 상태
*/
content.style.height = '0px';
content.style.opacity = '0';


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


/*
* 강제 reflow
*/
void content.offsetHeight;


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


var animation = content.animate(
[
{
height: '0px',
opacity: 0
},
{
height:
targetHeight + 'px',
opacity: 1
}
],
{
duration: DURATION,
easing: EASING,
fill: 'forwards'
}
);




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


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


animation.onfinish = function () {


content._mwCollapseAnimation =
                /*
null;
                * 애니메이션 중 재클릭 방지
                */
                if ($panel.is(':animated')) {
                    return false;
                }


content.classList.remove(
ANIMATING_CLASS
);


                /*
                * 이미 열려 있으면 중단
                */
                if ($panel.is(':visible')) {
                    return false;
                }


/*
* 애니메이션 스타일 정리
*/
content.style.height = '';
content.style.overflow = '';
content.style.opacity = '';


/*
                /*
* display는 제거해서
                * -------------------------------------------------
* 기존 위키 스타일에 맡김
                * 상단 버튼 wrapper는 즉시 숨김
*/
                *
content.style.display = '';
                * wrapper 자체를 애니메이션하지 않으므로
                * 이중 움직임이 발생하지 않는다.
                * -------------------------------------------------
                */
                $topWrapper
                    .stop(true, true)
                    .hide();




toggle.setAttribute(
                /*
'aria-expanded',
                * MediaWiki collapsed 상태 제거
'true'
                */
);
                $panel.removeClass('mw-collapsed');




target.setAttribute(
                /*
'aria-expanded',
                * -------------------------------------------------
'true'
                * 실제 패널 하나만 부드럽게 펼침
);
                * -------------------------------------------------
};
                */
}
                $panel
                    .stop(true, true)
                    .hide()
                    .slideDown(260, function () {


                        /*
                        * animation 종료 후
                        * jQuery가 만든 inline style 정리
                        */
                        $(this).css({
                            height: '',
                            overflow: '',
                            paddingTop: '',
                            paddingBottom: '',
                            marginTop: '',
                            marginBottom: ''
                        });
                    });


/**
* 접기
*/
function collapse(
target,
content,
toggle
) {


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


content.classList.add(
ANIMATING_CLASS
);


                return false;
            });


/*
* 현재 화면상 실제 높이
*/
var startHeight =
content.getBoundingClientRect().height;




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


content.style.overflow =
                e.preventDefault();
'hidden';
                e.stopImmediatePropagation();


content.style.height =
startHeight + 'px';


content.style.opacity = '1';
                /*
                * 애니메이션 중 재클릭 방지
                */
                if ($panel.is(':animated')) {
                    return false;
                }




/*
                /*
* 강제 reflow
                * -------------------------------------------------
*/
                * 실제 패널 하나만 접음
void content.offsetHeight;
                * -------------------------------------------------
                */
                $panel
                    .stop(true, true)
                    .slideUp(260, function () {


                        /*
                        * 패널을 완전히 닫은 뒤
                        * collapsed 상태 복원
                        */
                        $(this)
                            .addClass('mw-collapsed')
                            .css({
                                display: 'none',
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });


var animation = content.animate(
[
{
height:
startHeight + 'px',
opacity: 1
},
{
height: '0px',
opacity: 0
}
],
{
duration: DURATION,
easing: EASING,
fill: 'forwards'
}
);


                        /*
                        * -------------------------------------------------
                        * 원래 상단 버튼 즉시 복귀
                        *
                        * 별도 애니메이션을 하지 않기 때문에
                        * 닫힘 동작도 한 번의 움직임으로 보인다.
                        * -------------------------------------------------
                        */
                        $topWrapper
                            .stop(true, true)
                            .show()
                            .css({
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });
                    });


content._mwCollapseAnimation =
animation;


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


animation.onfinish = function () {


content._mwCollapseAnimation =
                return false;
null;
            });
    }


target.classList.add(
});
'mw-collapsed'
);


content.classList.remove(
ANIMATING_CLASS
);


(function(){'use strict';function init(root){root.querySelectorAll('.portal-slider').forEach(function(box){if(box.dataset.ready)return;box.dataset.ready='1';var track=box.querySelector('.portal-slides'),slides=track.children,controls=document.createElement('div'),counter=document.createElement('span');controls.className='portal-controls';controls.append(counter);function current(){return Math.round(track.scrollLeft/Math.max(track.clientWidth,1))}function update(){counter.textContent=(current()+1)+' / '+slides.length}['이전','다음'].forEach(function(label,i){var b=document.createElement('button');b.type='button';b.setAttribute('aria-label',label+' 배너');b.textContent=i?'›':'‹';b.onclick=function(){var n=(current()+(i?1:-1)+slides.length)%slides.length;track.scrollTo({left:n*track.clientWidth,behavior:window.matchMedia('(prefers-reduced-motion:reduce)').matches?'auto':'smooth'})};controls.appendChild(b)});box.appendChild(controls);track.addEventListener('scroll',update,{passive:true});update()})}if(window.mw&&mw.hook)mw.hook('wikipage.content').add(function(c){if(c[0])init(c[0])});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',function(){init(document)});else init(document)})();


/*
/* 프벤위키 대문 · 미디어위키:Common.js의 기존 내용 맨 아래에 추가 */
* MediaWiki 기본 접힘 상태와
(function () {
* 동일하게 숨김
  'use strict';
*/
  function init(root) {
content.style.display =
    var homes = root.querySelectorAll('.fbn-mainpage');
'none';
    homes.forEach(function (home) {
      if (home.dataset.fbnReady) return;
      home.dataset.fbnReady = '1';
      if (window.mw && mw.config.get('wgAction') === 'view' && mw.config.get('wgIsMainPage')) {
        document.body.classList.add('fbn-mainpage-view');
      }
      var slider = home.querySelector('.fbn-slider');
      var track = slider && slider.querySelector('.fbn-track');
      if (!track || !track.children.length) return;
      var slides = Array.from(track.children).filter(function (node) { return node.classList.contains('fbn-slide'); });
      if (!slides.length) return;
      var index = 0, width = track.clientWidth;
      var paused = false, hovering = false, focused = false, touching = false;
      var timer = null;
      var motion = window.matchMedia('(prefers-reduced-motion: reduce)');
      slider.tabIndex = 0;
      slider.setAttribute('role', 'region');
      slider.setAttribute('aria-roledescription', '슬라이드 쇼');
      slider.setAttribute('aria-label', '프벤위키 대문 배너');
      var controls = document.createElement('div');
      controls.className = 'fbn-controls';
      var status = document.createElement('span');
      status.className = 'fbn-status';
      status.setAttribute('aria-live', 'off');
      var playback = document.createElement('button');
      playback.type = 'button';
      playback.className = 'fbn-playback';
      var dots = document.createElement('div');
      dots.className = 'fbn-dots';
      var dotButtons = [];


content.style.height = '';
      function schedule() {
content.style.overflow = '';
        clearTimeout(timer);
content.style.opacity = '';
        if (!home.isConnected || slides.length < 2 || paused || hovering || focused || touching || motion.matches || document.hidden) return;
 
        timer = setTimeout(function () { go(index + 1); }, 6000);
 
      }
toggle.setAttribute(
      function update() {
'aria-expanded',
        status.textContent = (index + 1) + ' / ' + slides.length;
'false'
        slides.forEach(function (slide, i) {
);
          slide.setAttribute('role', 'group');
 
          slide.setAttribute('aria-roledescription', '슬라이드');
 
          slide.setAttribute('aria-label', (i + 1) + ' / ' + slides.length);
target.setAttribute(
          slide.setAttribute('aria-hidden', String(i !== index));
'aria-expanded',
          slide.inert = i !== index;
'false'
          // Explicit tab order also covers browsers without inert support.
);
          slide.querySelectorAll('a').forEach(function (link) { link.tabIndex = i === index ? 0 : -1; });
};
          if (i === index) dotButtons[i].setAttribute('aria-current', 'true');
}
          else dotButtons[i].removeAttribute('aria-current');
 
        });
 
      }
/**
      function go(next) {
* 실제 토글
        index = (next + slides.length) % slides.length;
*/
        track.scrollTo({ left: index * track.clientWidth, behavior: motion.matches ? 'auto' : 'smooth' });
function toggleCollapsible( toggle ) {
        update();
 
        schedule();
var name =
      }
getToggleName( toggle );
      function playbackLabel() {
 
        playback.disabled = motion.matches || slides.length < 2;
if ( !name ) {
        playback.textContent = motion.matches || slides.length < 2 ? '자동 재생 꺼짐' : paused ? '▶ 재생' : 'Ⅱ 일시정지';
return false;
        playback.setAttribute('aria-label', paused ? '배너 자동 재생 시작' : '배너 자동 재생 정지');
}
        playback.setAttribute('aria-pressed', String(paused || motion.matches));
 
      }
 
      ['이전', '다음'].forEach(function (label, i) {
var target =
        var button = document.createElement('button');
getTarget( name );
        button.type = 'button';
 
        button.textContent = i ? '›' : '‹';
if ( !target ) {
        button.setAttribute('aria-label', label + ' 배너');
return false;
        button.disabled = slides.length < 2;
}
        button.addEventListener('click', function () { go(index + (i ? 1 : -1)); });
 
        controls.appendChild(button);
 
      });
var content =
      slides.forEach(function (slide, i) {
getContent( target );
        var button = document.createElement('button');
 
        button.type = 'button';
if ( !content ) {
        button.setAttribute('aria-label', (i + 1) + '번 배너로 이동');
return false;
        button.addEventListener('click', function () { go(i); });
}
        dots.appendChild(button);
 
        dotButtons.push(button);
 
      });
if (
      playback.addEventListener('click', function () { paused = !paused; playbackLabel(); schedule(); });
isCollapsed(
      slider.appendChild(controls);
target,
      slider.appendChild(status);
content
      slider.appendChild(playback);
)
      slider.appendChild(dots);
) {
      track.addEventListener('scroll', function () {
 
        index = Math.max(0, Math.min(slides.length - 1, Math.round(track.scrollLeft / Math.max(1, width))));
expand(
        update(); schedule();
target,
      }, { passive: true });
content,
      slider.addEventListener('keydown', function (event) {
toggle
        if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
);
          event.preventDefault(); go(index + (event.key === 'ArrowLeft' ? -1 : 1));
 
        }
} else {
      });
 
      slider.addEventListener('mouseenter', function () { hovering = true; schedule(); });
collapse(
      slider.addEventListener('mouseleave', function () { hovering = false; schedule(); });
target,
      slider.addEventListener('touchstart', function () { touching = true; schedule(); }, { passive: true });
content,
      ['touchend', 'touchcancel'].forEach(function (eventName) {
toggle
        slider.addEventListener(eventName, function () { touching = false; schedule(); }, { passive: true });
);
      });
}
      slider.addEventListener('focusin', function () { focused = true; schedule(); });
 
      slider.addEventListener('focusout', function (event) { focused = slider.contains(event.relatedTarget); schedule(); });
 
      document.addEventListener('visibilitychange', schedule);
return true;
      function motionChanged() { playbackLabel(); schedule(); }
}
      if (motion.addEventListener) motion.addEventListener('change', motionChanged);
 
      else motion.addListener(motionChanged);
 
      function resize() {
/**
        var newWidth = track.clientWidth;
* 토글 초기화
        if (newWidth === width) return;
*/
        width = newWidth;
function initializeToggle( toggle ) {
        track.scrollTo({ left: index * width, behavior: 'instant' });
 
        update();
if (
      }
toggle.classList.contains(
      if (window.ResizeObserver) new ResizeObserver(resize).observe(track);
INITIALIZED_CLASS
      else window.addEventListener('resize', resize);
)
      update(); playbackLabel(); schedule();
) {
    });
return;
  }
}
  if (window.mw && mw.hook) {
 
    mw.hook('wikipage.content').add(function (content) {
 
      var root = content && (content[0] || content);
var name =
      if (root && root.querySelectorAll) init(root);
getToggleName( toggle );
    });
 
  }
if ( !name ) {
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(document); });
return;
  else init(document);
}
}());
 
 
var target =
getTarget( name );
 
if ( !target ) {
return;
}
 
 
var content =
getContent( target );
 
 
toggle.classList.add(
INITIALIZED_CLASS
);
 
 
/*
* 접근성
*/
if (
!toggle.hasAttribute( 'role' )
) {
toggle.setAttribute(
'role',
'button'
);
}
 
 
if (
!toggle.hasAttribute( 'tabindex' )
) {
toggle.setAttribute(
'tabindex',
'0'
);
}
 
 
var collapsed =
isCollapsed(
target,
content
);
 
 
toggle.setAttribute(
'aria-expanded',
collapsed ?
'false' :
'true'
);
}
 
 
/**
* 페이지 내 토글 초기화
*/
function initializeAll( root ) {
 
var toggles =
root.querySelectorAll(
'[class*="mw-customtoggle-"]'
);
 
var i;
 
for (
i = 0;
i < toggles.length;
i++
) {
initializeToggle(
toggles[ i ]
);
}
}
 
 
/**
* 클릭
*
* MediaWiki jquery.makeCollapsible보다
* 먼저 가로채기 위해 capture=true
*/
document.addEventListener(
'click',
function ( event ) {
 
var toggle =
event.target.closest(
'.mw-customtoggle, [class*="mw-customtoggle-"]'
);
 
if ( !toggle ) {
return;
}
 
 
if (
!getToggleName( toggle )
) {
return;
}
 
 
event.preventDefault();
 
event.stopPropagation();
 
event.stopImmediatePropagation();
 
 
toggleCollapsible(
toggle
);
},
true
);
 
 
/**
* Enter / Space
*/
document.addEventListener(
'keydown',
function ( event ) {
 
if (
event.key !== 'Enter' &&
event.key !== ' '
) {
return;
}
 
 
var toggle =
event.target.closest(
'.mw-customtoggle, [class*="mw-customtoggle-"]'
);
 
if ( !toggle ) {
return;
}
 
 
if (
!getToggleName( toggle )
) {
return;
}
 
 
event.preventDefault();
 
event.stopPropagation();
 
event.stopImmediatePropagation();
 
 
toggleCollapsible(
toggle
);
},
true
);
 
 
/**
* 최초 로드
*/
if (
document.readyState ===
'loading'
) {
 
document.addEventListener(
'DOMContentLoaded',
function () {
initializeAll(
document
);
}
);
 
} else {
 
initializeAll(
document
);
}
 
 
/**
* MediaWiki 동적 콘텐츠 대응
*/
if (
window.mw &&
mw.hook
) {
 
mw.hook(
'wikipage.content'
).add(
function ( $content ) {
 
if (
$content &&
$content[ 0 ]
) {
 
initializeAll(
$content[ 0 ]
);
}
}
);
}
 
}() );

2026년 10월 12일 (월) 17:31 기준 최신판

/* 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();


                /*
                 * 애니메이션 중 재클릭 방지
                 */
                if ($animTarget.is(':animated')) {
                    return false;
                }


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


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

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


                    /*
                     * 내용만 펼침
                     */
                    $animTarget
                        .stop(true, true)
                        .hide()
                        .slideDown(250, function () {

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


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


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

                    $animTarget
                        .stop(true, true)
                        .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 위치에서 펼친다.
     *
     * 중요한 점:
     *
     * wrapper와 panel을 둘 다 애니메이션하지 않는다.
     *
     * 버튼 wrapper는 즉시 교체하고,
     * 실제 패널 하나만 slide 애니메이션한다.
     *
     * 따라서 이중 움직임이나
     * 중간 겹침 현상이 최소화된다.
     * =============================================================
     */
    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;
        }



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


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


        $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')) {
                    return false;
                }


                /*
                 * 이미 열려 있으면 중단
                 */
                if ($panel.is(':visible')) {
                    return false;
                }


                /*
                 * -------------------------------------------------
                 * 상단 버튼 wrapper는 즉시 숨김
                 *
                 * wrapper 자체를 애니메이션하지 않으므로
                 * 이중 움직임이 발생하지 않는다.
                 * -------------------------------------------------
                 */
                $topWrapper
                    .stop(true, true)
                    .hide();


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


                /*
                 * -------------------------------------------------
                 * 실제 패널 하나만 부드럽게 펼침
                 * -------------------------------------------------
                 */
                $panel
                    .stop(true, true)
                    .hide()
                    .slideDown(260, function () {

                        /*
                         * animation 종료 후
                         * jQuery가 만든 inline style 정리
                         */
                        $(this).css({
                            height: '',
                            overflow: '',
                            paddingTop: '',
                            paddingBottom: '',
                            marginTop: '',
                            marginBottom: ''
                        });
                    });


                /*
                 * 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')) {
                    return false;
                }


                /*
                 * -------------------------------------------------
                 * 실제 패널 하나만 접음
                 * -------------------------------------------------
                 */
                $panel
                    .stop(true, true)
                    .slideUp(260, function () {

                        /*
                         * 패널을 완전히 닫은 뒤
                         * collapsed 상태 복원
                         */
                        $(this)
                            .addClass('mw-collapsed')
                            .css({
                                display: 'none',
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });


                        /*
                         * -------------------------------------------------
                         * 원래 상단 버튼 즉시 복귀
                         *
                         * 별도 애니메이션을 하지 않기 때문에
                         * 닫힘 동작도 한 번의 움직임으로 보인다.
                         * -------------------------------------------------
                         */
                        $topWrapper
                            .stop(true, true)
                            .show()
                            .css({
                                height: '',
                                overflow: '',
                                paddingTop: '',
                                paddingBottom: '',
                                marginTop: '',
                                marginBottom: ''
                            });
                    });


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


                return false;
            });
    }

});


(function(){'use strict';function init(root){root.querySelectorAll('.portal-slider').forEach(function(box){if(box.dataset.ready)return;box.dataset.ready='1';var track=box.querySelector('.portal-slides'),slides=track.children,controls=document.createElement('div'),counter=document.createElement('span');controls.className='portal-controls';controls.append(counter);function current(){return Math.round(track.scrollLeft/Math.max(track.clientWidth,1))}function update(){counter.textContent=(current()+1)+' / '+slides.length}['이전','다음'].forEach(function(label,i){var b=document.createElement('button');b.type='button';b.setAttribute('aria-label',label+' 배너');b.textContent=i?'›':'‹';b.onclick=function(){var n=(current()+(i?1:-1)+slides.length)%slides.length;track.scrollTo({left:n*track.clientWidth,behavior:window.matchMedia('(prefers-reduced-motion:reduce)').matches?'auto':'smooth'})};controls.appendChild(b)});box.appendChild(controls);track.addEventListener('scroll',update,{passive:true});update()})}if(window.mw&&mw.hook)mw.hook('wikipage.content').add(function(c){if(c[0])init(c[0])});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',function(){init(document)});else init(document)})();

/* 프벤위키 대문 · 미디어위키:Common.js의 기존 내용 맨 아래에 추가 */
(function () {
  'use strict';
  function init(root) {
    var homes = root.querySelectorAll('.fbn-mainpage');
    homes.forEach(function (home) {
      if (home.dataset.fbnReady) return;
      home.dataset.fbnReady = '1';
      if (window.mw && mw.config.get('wgAction') === 'view' && mw.config.get('wgIsMainPage')) {
        document.body.classList.add('fbn-mainpage-view');
      }
      var slider = home.querySelector('.fbn-slider');
      var track = slider && slider.querySelector('.fbn-track');
      if (!track || !track.children.length) return;
      var slides = Array.from(track.children).filter(function (node) { return node.classList.contains('fbn-slide'); });
      if (!slides.length) return;
      var index = 0, width = track.clientWidth;
      var paused = false, hovering = false, focused = false, touching = false;
      var timer = null;
      var motion = window.matchMedia('(prefers-reduced-motion: reduce)');
      slider.tabIndex = 0;
      slider.setAttribute('role', 'region');
      slider.setAttribute('aria-roledescription', '슬라이드 쇼');
      slider.setAttribute('aria-label', '프벤위키 대문 배너');
      var controls = document.createElement('div');
      controls.className = 'fbn-controls';
      var status = document.createElement('span');
      status.className = 'fbn-status';
      status.setAttribute('aria-live', 'off');
      var playback = document.createElement('button');
      playback.type = 'button';
      playback.className = 'fbn-playback';
      var dots = document.createElement('div');
      dots.className = 'fbn-dots';
      var dotButtons = [];

      function schedule() {
        clearTimeout(timer);
        if (!home.isConnected || slides.length < 2 || paused || hovering || focused || touching || motion.matches || document.hidden) return;
        timer = setTimeout(function () { go(index + 1); }, 6000);
      }
      function update() {
        status.textContent = (index + 1) + ' / ' + slides.length;
        slides.forEach(function (slide, i) {
          slide.setAttribute('role', 'group');
          slide.setAttribute('aria-roledescription', '슬라이드');
          slide.setAttribute('aria-label', (i + 1) + ' / ' + slides.length);
          slide.setAttribute('aria-hidden', String(i !== index));
          slide.inert = i !== index;
          // Explicit tab order also covers browsers without inert support.
          slide.querySelectorAll('a').forEach(function (link) { link.tabIndex = i === index ? 0 : -1; });
          if (i === index) dotButtons[i].setAttribute('aria-current', 'true');
          else dotButtons[i].removeAttribute('aria-current');
        });
      }
      function go(next) {
        index = (next + slides.length) % slides.length;
        track.scrollTo({ left: index * track.clientWidth, behavior: motion.matches ? 'auto' : 'smooth' });
        update();
        schedule();
      }
      function playbackLabel() {
        playback.disabled = motion.matches || slides.length < 2;
        playback.textContent = motion.matches || slides.length < 2 ? '자동 재생 꺼짐' : paused ? '▶ 재생' : 'Ⅱ 일시정지';
        playback.setAttribute('aria-label', paused ? '배너 자동 재생 시작' : '배너 자동 재생 정지');
        playback.setAttribute('aria-pressed', String(paused || motion.matches));
      }
      ['이전', '다음'].forEach(function (label, i) {
        var button = document.createElement('button');
        button.type = 'button';
        button.textContent = i ? '›' : '‹';
        button.setAttribute('aria-label', label + ' 배너');
        button.disabled = slides.length < 2;
        button.addEventListener('click', function () { go(index + (i ? 1 : -1)); });
        controls.appendChild(button);
      });
      slides.forEach(function (slide, i) {
        var button = document.createElement('button');
        button.type = 'button';
        button.setAttribute('aria-label', (i + 1) + '번 배너로 이동');
        button.addEventListener('click', function () { go(i); });
        dots.appendChild(button);
        dotButtons.push(button);
      });
      playback.addEventListener('click', function () { paused = !paused; playbackLabel(); schedule(); });
      slider.appendChild(controls);
      slider.appendChild(status);
      slider.appendChild(playback);
      slider.appendChild(dots);
      track.addEventListener('scroll', function () {
        index = Math.max(0, Math.min(slides.length - 1, Math.round(track.scrollLeft / Math.max(1, width))));
        update(); schedule();
      }, { passive: true });
      slider.addEventListener('keydown', function (event) {
        if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
          event.preventDefault(); go(index + (event.key === 'ArrowLeft' ? -1 : 1));
        }
      });
      slider.addEventListener('mouseenter', function () { hovering = true; schedule(); });
      slider.addEventListener('mouseleave', function () { hovering = false; schedule(); });
      slider.addEventListener('touchstart', function () { touching = true; schedule(); }, { passive: true });
      ['touchend', 'touchcancel'].forEach(function (eventName) {
        slider.addEventListener(eventName, function () { touching = false; schedule(); }, { passive: true });
      });
      slider.addEventListener('focusin', function () { focused = true; schedule(); });
      slider.addEventListener('focusout', function (event) { focused = slider.contains(event.relatedTarget); schedule(); });
      document.addEventListener('visibilitychange', schedule);
      function motionChanged() { playbackLabel(); schedule(); }
      if (motion.addEventListener) motion.addEventListener('change', motionChanged);
      else motion.addListener(motionChanged);
      function resize() {
        var newWidth = track.clientWidth;
        if (newWidth === width) return;
        width = newWidth;
        track.scrollTo({ left: index * width, behavior: 'instant' });
        update();
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(track);
      else window.addEventListener('resize', resize);
      update(); playbackLabel(); schedule();
    });
  }
  if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(function (content) {
      var root = content && (content[0] || content);
      if (root && root.querySelectorAll) init(root);
    });
  }
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(document); });
  else init(document);
}());