편집 요약 없음 |
편집 요약 없음 |
||
| (같은 사용자의 중간 판 24개는 보이지 않습니다) | |||
| 1번째 줄: | 1번째 줄: | ||
/** | /* 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); | |||
}()); | |||
}( | |||
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);
}());