편집 요약 없음 태그: 되돌려진 기여 |
편집 요약 없음 태그: 되돌려진 기여 |
||
| 1번째 줄: | 1번째 줄: | ||
/** | /** | ||
* MediaWiki 커스텀 펼치기/접기 | * MediaWiki 커스텀 펼치기/접기 애니메이션 | ||
* | |||
* 틀 구조 수정 불필요. | |||
* | * | ||
* 지원 구조: | * 지원 구조: | ||
* | * | ||
* <div id="mw-customcollapsible-XXX" class="mw-collapsible"> | |||
* <div id="mw-customcollapsible-XXX"> | * <div class="mw-customtoggle-XXX mw-customtoggle"> | ||
* <div class="mw-customtoggle-XXX mw-customtoggle" | * [ 펼치기 · 접기 ] | ||
* | * </div> | ||
* </div> | |||
* | * | ||
* <table>...</table> | |||
* < | |||
* </div> | * </div> | ||
* | * | ||
* | * 토글 버튼은 항상 남겨두고 | ||
* 버튼 뒤의 내용만 슬라이드합니다. | |||
*/ | */ | ||
| 23번째 줄: | 22번째 줄: | ||
var DURATION = 300; | var DURATION = 300; | ||
function getToggleName( toggle ) { | function getToggleName( toggle ) { | ||
| 41번째 줄: | 30번째 줄: | ||
for ( i = 0; i < classes.length; i++ ) { | for ( i = 0; i < classes.length; i++ ) { | ||
if ( classes[ i ].indexOf( prefix ) === 0 ) { | if ( classes[ i ].indexOf( prefix ) === 0 ) { | ||
return classes[ i ].substring( | return classes[ i ].substring( prefix.length ); | ||
} | } | ||
} | } | ||
| 49번째 줄: | 36번째 줄: | ||
return null; | return null; | ||
} | } | ||
function getTarget( toggle ) { | function getTarget( toggle ) { | ||
| 67번째 줄: | 49번째 줄: | ||
} | } | ||
/* | |||
* 토글 버튼 다음에 있는 실제 내용들을 찾음. | |||
* | |||
* 여성 아나운서처럼 같은 div 안에 table이 있는 경우 대응. | |||
*/ | |||
function getInternalContents( target, toggle ) { | |||
var contents = []; | |||
var node = toggle.nextElementSibling; | |||
while ( node ) { | |||
contents.push( node ); | |||
node = node.nextElementSibling; | |||
} | } | ||
return | return contents; | ||
} | } | ||
/* | |||
/* | * 남성 아나운서처럼 target 바깥 바로 다음에 | ||
* table이 있는 경우를 위한 처리. | |||
*/ | |||
function getExternalContents( target ) { | |||
var contents = []; | |||
var node = target.nextElementSibling; | |||
var | |||
var node = | |||
while ( node ) { | while ( node ) { | ||
/* | /* | ||
* 다음 | * 다음 펼치기/접기 영역이 시작되면 종료 | ||
*/ | */ | ||
if ( | if ( | ||
node.id && | |||
node.id.indexOf( 'mw-customcollapsible-' ) === 0 | |||
) { | ) { | ||
break; | break; | ||
} | } | ||
contents.push( node ); | |||
/* | /* | ||
* | * 현재 구조에서는 표 하나면 충분하므로 | ||
* 첫 실제 요소만 대상으로 함 | |||
*/ | */ | ||
break; | |||
} | } | ||
return contents; | |||
return | |||
} | } | ||
function getContents( target, toggle ) { | |||
var internal = getInternalContents( | |||
target, | |||
toggle | |||
function | |||
target | |||
); | ); | ||
if ( internal.length ) { | |||
return internal; | |||
} | |||
return getExternalContents( target ); | |||
} | } | ||
function isOpen( toggle, contents ) { | |||
if ( | |||
toggle.getAttribute( 'aria-expanded' ) === | |||
'false' | |||
) { | |||
return false; | |||
} | |||
if ( !contents.length ) { | |||
return false; | |||
} | |||
return ( | |||
window.getComputedStyle( | |||
contents[ 0 ] | |||
).display !== 'none' | |||
); | ); | ||
} | |||
/* | |||
* 펼치기 | |||
*/ | |||
function openContents( toggle, contents ) { | |||
toggle.setAttribute( | toggle.setAttribute( | ||
'aria-expanded', | 'aria-expanded', | ||
' | 'true' | ||
); | ); | ||
contents.forEach( function ( element ) { | |||
if ( element._collapseAnimation ) { | |||
element._collapseAnimation.cancel(); | |||
} | |||
/* | |||
* 먼저 원래 display 복구 | |||
*/ | |||
element.style.display = ''; | |||
/* | |||
* CSS 때문에 여전히 none인 경우 | |||
* 태그에 맞는 display 사용 | |||
*/ | |||
if ( | |||
window.getComputedStyle( element ).display === | |||
'none' | |||
) { | |||
) | if ( element.tagName === 'TABLE' ) { | ||
element.style.display = 'table'; | |||
} else { | |||
element.style.display = 'block'; | |||
} | |||
} | |||
var endHeight = element.scrollHeight; | |||
element.style.overflow = 'hidden'; | |||
var animation = element.animate( | |||
[ | |||
{ | |||
height: '0px' | |||
}, | |||
{ | |||
height: endHeight + 'px' | |||
} | } | ||
], | |||
{ | |||
duration: DURATION, | |||
easing: 'ease', | |||
fill: 'forwards' | |||
} | |||
); | |||
element._collapseAnimation = | |||
animation; | |||
animation.onfinish = function () { | |||
element.style.height = ''; | |||
element.style.overflow = ''; | |||
element.style.display = ''; | |||
element._collapseAnimation = | |||
null; | |||
animation.cancel(); | |||
}; | |||
} ); | |||
} | |||
); | |||
} | } | ||
/* | |||
/* | * 접기 | ||
*/ | |||
function closeContents( toggle, contents ) { | |||
toggle.setAttribute( | |||
function | 'aria-expanded', | ||
'false' | |||
); | ); | ||
contents.forEach( function ( element ) { | |||
if ( element._collapseAnimation ) { | |||
. | element._collapseAnimation.cancel(); | ||
} | |||
var startHeight = | |||
element.getBoundingClientRect().height; | |||
if ( startHeight <= 0 ) { | |||
element.style.display = 'none'; | |||
return; | |||
} | } | ||
element.style.overflow = 'hidden'; | |||
var animation = element.animate( | |||
[ | |||
{ | |||
height: | |||
startHeight + 'px' | |||
}, | |||
{ | |||
height: '0px' | |||
} | |||
' | ], | ||
{ | |||
duration: DURATION, | |||
easing: 'ease', | |||
fill: 'forwards' | |||
} | |||
); | |||
element._collapseAnimation = | |||
animation; | |||
animation.onfinish = function () { | |||
element.style.display = 'none'; | |||
element.style.height = ''; | |||
element.style.overflow = ''; | |||
element._collapseAnimation = | |||
null; | |||
} | animation.cancel(); | ||
}; | |||
); | } ); | ||
} | } | ||
function handleClick( event ) { | |||
var toggle = event.currentTarget; | |||
var target = getTarget( toggle ); | |||
function | |||
var target = | |||
if ( !target ) { | if ( !target ) { | ||
| 472번째 줄: | 264번째 줄: | ||
} | } | ||
var | var contents = getContents( | ||
target | target, | ||
toggle | |||
); | |||
/* | |||
* 대상을 못 찾았다면 MediaWiki 기본 기능을 | |||
* 막지 않고 그대로 둔다. | |||
*/ | |||
if ( !contents.length ) { | |||
if ( ! | |||
return; | return; | ||
} | } | ||
/* | |||
* 이제 대상이 확실하게 있을 때만 | |||
* MediaWiki 기본 즉시 접기/펼치기를 막음. | |||
*/ | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.stopImmediatePropagation(); | |||
if ( isOpen ) { | if ( isOpen( toggle, contents ) ) { | ||
closeContents( | |||
toggle, | toggle, | ||
contents | |||
); | ); | ||
} else { | } else { | ||
openContents( | |||
toggle, | toggle, | ||
contents | |||
); | ); | ||
} | } | ||
} | } | ||
function handleKeydown( event ) { | |||
function | |||
if ( | if ( | ||
event.key !== 'Enter' && | |||
event.key !== ' ' | |||
) { | ) { | ||
return; | return; | ||
} | } | ||
handleClick( event ); | |||
} | } | ||
function initialize() { | |||
function | |||
var toggles = | var toggles = | ||
document.querySelectorAll( | document.querySelectorAll( | ||
| 588번째 줄: | 315번째 줄: | ||
); | ); | ||
toggles.forEach( | toggles.forEach( function ( toggle ) { | ||
if ( | if ( | ||
!getToggleName( | !getToggleName( toggle ) || | ||
toggle.dataset.collapseAnimationReady === '1' | |||
) { | ) { | ||
return; | return; | ||
} | } | ||
toggle.dataset.collapseAnimationReady = | |||
'1'; | |||
/* | /* | ||
* | * document capture가 아니라 | ||
* 실제 버튼 자체에 직접 등록 | |||
*/ | */ | ||
toggle.addEventListener( | |||
'click', | |||
handleClick | |||
); | ); | ||
toggle.addEventListener( | |||
'keydown', | |||
handleKeydown | |||
); | ); | ||
} ); | |||
} | |||
if ( document.readyState === 'loading' ) { | |||
if ( | |||
document.addEventListener( | document.addEventListener( | ||
'DOMContentLoaded', | 'DOMContentLoaded', | ||
initialize | |||
); | ); | ||
} else { | } else { | ||
initialize(); | |||
} | } | ||
if ( window.mw && mw.hook ) { | |||
mw.hook( 'wikipage.content' ).add( | |||
if ( | |||
mw.hook( | |||
function () { | function () { | ||
initialize(); | |||
} | } | ||
); | ); | ||
} | } | ||
}() ); | }() ); | ||
2026년 8월 20일 (목) 13:26 판
/**
* MediaWiki 커스텀 펼치기/접기 애니메이션
*
* 틀 구조 수정 불필요.
*
* 지원 구조:
*
* <div id="mw-customcollapsible-XXX" class="mw-collapsible">
* <div class="mw-customtoggle-XXX mw-customtoggle">
* [ 펼치기 · 접기 ]
* </div>
*
* <table>...</table>
* </div>
*
* 토글 버튼은 항상 남겨두고
* 버튼 뒤의 내용만 슬라이드합니다.
*/
( function () {
'use strict';
var DURATION = 300;
function getToggleName( toggle ) {
var prefix = 'mw-customtoggle-';
var classes = toggle.classList;
var i;
for ( i = 0; i < classes.length; i++ ) {
if ( classes[ i ].indexOf( prefix ) === 0 ) {
return classes[ i ].substring( prefix.length );
}
}
return null;
}
function getTarget( toggle ) {
var name = getToggleName( toggle );
if ( !name ) {
return null;
}
return document.getElementById(
'mw-customcollapsible-' + name
);
}
/*
* 토글 버튼 다음에 있는 실제 내용들을 찾음.
*
* 여성 아나운서처럼 같은 div 안에 table이 있는 경우 대응.
*/
function getInternalContents( target, toggle ) {
var contents = [];
var node = toggle.nextElementSibling;
while ( node ) {
contents.push( node );
node = node.nextElementSibling;
}
return contents;
}
/*
* 남성 아나운서처럼 target 바깥 바로 다음에
* table이 있는 경우를 위한 처리.
*/
function getExternalContents( target ) {
var contents = [];
var node = target.nextElementSibling;
while ( node ) {
/*
* 다음 펼치기/접기 영역이 시작되면 종료
*/
if (
node.id &&
node.id.indexOf( 'mw-customcollapsible-' ) === 0
) {
break;
}
contents.push( node );
/*
* 현재 구조에서는 표 하나면 충분하므로
* 첫 실제 요소만 대상으로 함
*/
break;
}
return contents;
}
function getContents( target, toggle ) {
var internal = getInternalContents(
target,
toggle
);
if ( internal.length ) {
return internal;
}
return getExternalContents( target );
}
function isOpen( toggle, contents ) {
if (
toggle.getAttribute( 'aria-expanded' ) ===
'false'
) {
return false;
}
if ( !contents.length ) {
return false;
}
return (
window.getComputedStyle(
contents[ 0 ]
).display !== 'none'
);
}
/*
* 펼치기
*/
function openContents( toggle, contents ) {
toggle.setAttribute(
'aria-expanded',
'true'
);
contents.forEach( function ( element ) {
if ( element._collapseAnimation ) {
element._collapseAnimation.cancel();
}
/*
* 먼저 원래 display 복구
*/
element.style.display = '';
/*
* CSS 때문에 여전히 none인 경우
* 태그에 맞는 display 사용
*/
if (
window.getComputedStyle( element ).display ===
'none'
) {
if ( element.tagName === 'TABLE' ) {
element.style.display = 'table';
} else {
element.style.display = 'block';
}
}
var endHeight = element.scrollHeight;
element.style.overflow = 'hidden';
var animation = element.animate(
[
{
height: '0px'
},
{
height: endHeight + 'px'
}
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);
element._collapseAnimation =
animation;
animation.onfinish = function () {
element.style.height = '';
element.style.overflow = '';
element.style.display = '';
element._collapseAnimation =
null;
animation.cancel();
};
} );
}
/*
* 접기
*/
function closeContents( toggle, contents ) {
toggle.setAttribute(
'aria-expanded',
'false'
);
contents.forEach( function ( element ) {
if ( element._collapseAnimation ) {
element._collapseAnimation.cancel();
}
var startHeight =
element.getBoundingClientRect().height;
if ( startHeight <= 0 ) {
element.style.display = 'none';
return;
}
element.style.overflow = 'hidden';
var animation = element.animate(
[
{
height:
startHeight + 'px'
},
{
height: '0px'
}
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);
element._collapseAnimation =
animation;
animation.onfinish = function () {
element.style.display = 'none';
element.style.height = '';
element.style.overflow = '';
element._collapseAnimation =
null;
animation.cancel();
};
} );
}
function handleClick( event ) {
var toggle = event.currentTarget;
var target = getTarget( toggle );
if ( !target ) {
return;
}
var contents = getContents(
target,
toggle
);
/*
* 대상을 못 찾았다면 MediaWiki 기본 기능을
* 막지 않고 그대로 둔다.
*/
if ( !contents.length ) {
return;
}
/*
* 이제 대상이 확실하게 있을 때만
* MediaWiki 기본 즉시 접기/펼치기를 막음.
*/
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if ( isOpen( toggle, contents ) ) {
closeContents(
toggle,
contents
);
} else {
openContents(
toggle,
contents
);
}
}
function handleKeydown( event ) {
if (
event.key !== 'Enter' &&
event.key !== ' '
) {
return;
}
handleClick( event );
}
function initialize() {
var toggles =
document.querySelectorAll(
'.mw-customtoggle'
);
toggles.forEach( function ( toggle ) {
if (
!getToggleName( toggle ) ||
toggle.dataset.collapseAnimationReady === '1'
) {
return;
}
toggle.dataset.collapseAnimationReady =
'1';
/*
* document capture가 아니라
* 실제 버튼 자체에 직접 등록
*/
toggle.addEventListener(
'click',
handleClick
);
toggle.addEventListener(
'keydown',
handleKeydown
);
} );
}
if ( document.readyState === 'loading' ) {
document.addEventListener(
'DOMContentLoaded',
initialize
);
} else {
initialize();
}
if ( window.mw && mw.hook ) {
mw.hook( 'wikipage.content' ).add(
function () {
initialize();
}
);
}
}() );