편집 요약 없음 태그: 되돌려진 기여 |
편집 요약 없음 태그: 수동 되돌리기 |
||
| 2번째 줄: | 2번째 줄: | ||
* MediaWiki 커스텀 펼치기/접기 애니메이션 | * MediaWiki 커스텀 펼치기/접기 애니메이션 | ||
* | * | ||
* | * 지원 예: | ||
* | * | ||
* | * .mw-customtoggle-KSC_LEADER | ||
* ↕ | |||
* .mw-customcollapsible-KSC_LEADER | |||
* | * | ||
* | * 식별자 이름은 자유롭게 사용 가능. | ||
*/ | */ | ||
| 22번째 줄: | 15번째 줄: | ||
var DURATION = 300; | var DURATION = 300; | ||
var ANIMATING_CLASS = 'mw-custom-collapse-animating'; | |||
/** | |||
* 토글 요소에서 식별자 추출 | |||
* | |||
* 예: | |||
* mw-customtoggle-KSC_LEADER | |||
* → KSC_LEADER | |||
*/ | |||
function getToggleName( toggle ) { | function getToggleName( toggle ) { | ||
var prefix = 'mw-customtoggle-'; | var prefix = 'mw-customtoggle-'; | ||
| 37번째 줄: | 38번째 줄: | ||
} | } | ||
function | /** | ||
* CSS selector escape | |||
*/ | |||
function escapeSelector( value ) { | |||
if ( window.CSS && CSS.escape ) { | |||
return CSS.escape( value ); | |||
} | |||
return value.replace( | |||
/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, | |||
'\\\\$1' | |||
); | |||
} | |||
/** | |||
* 식별자에 대응하는 collapsible 대상 찾기 | |||
*/ | |||
function getTarget( name ) { | |||
if ( !name ) { | if ( !name ) { | ||
return null; | return null; | ||
} | } | ||
return document.getElementById( | var target = document.querySelector( | ||
'.mw-customcollapsible-' + | |||
escapeSelector( name ) | |||
); | |||
if ( target ) { | |||
return target; | |||
} | |||
target = document.getElementById( | |||
'mw-customcollapsible-' + name | 'mw-customcollapsible-' + name | ||
); | ); | ||
return target; | |||
} | } | ||
/* | /** | ||
* | * 실제 애니메이션을 적용할 콘텐츠 찾기 | ||
*/ | */ | ||
function | function getContent( target ) { | ||
var | var content = target.querySelector( | ||
'.mw-collapsible-content' | |||
); | |||
if ( content ) { | |||
return content; | |||
} | } | ||
return | return target; | ||
} | } | ||
/* | /** | ||
* | * 요소 원래 display 타입 보존 | ||
* | * | ||
* table 요소에 display:block을 강제하면 | |||
* 레이아웃이 깨질 수 있으므로 태그별 기본값 사용. | |||
*/ | */ | ||
function | function getNaturalDisplay( element ) { | ||
switch ( element.tagName ) { | |||
case 'TABLE': | |||
return 'table'; | |||
case 'TBODY': | |||
return 'table-row-group'; | |||
case 'THEAD': | |||
return 'table-header-group'; | |||
case 'TFOOT': | |||
return 'table-footer-group'; | |||
case 'TR': | |||
return 'table-row'; | |||
case 'TD': | |||
case 'TH': | |||
return 'table-cell'; | |||
case 'COLGROUP': | |||
return 'table-column-group'; | |||
case 'COL': | |||
return 'table-column'; | |||
case 'CAPTION': | |||
return 'table-caption'; | |||
case 'LI': | |||
return 'list-item'; | |||
default: | |||
return 'block'; | |||
} | } | ||
} | |||
if ( | /** | ||
return | * 현재 접혀있는지 확인 | ||
*/ | |||
function isCollapsed( target, content ) { | |||
if ( target.classList.contains( 'mw-collapsed' ) ) { | |||
return true; | |||
} | } | ||
return | return window.getComputedStyle( content ).display === 'none'; | ||
} | } | ||
/* | /** | ||
* 펼치기 | * 펼치기 | ||
*/ | */ | ||
function | function expand( target, content, toggle ) { | ||
if ( content.classList.contains( ANIMATING_CLASS ) ) { | |||
return; | |||
} | |||
); | |||
content.classList.add( ANIMATING_CLASS ); | |||
target.classList.remove( 'mw-collapsed' ); | |||
/* | |||
* display:none 상태에서는 높이를 구할 수 없으므로 | |||
* 원래 요소 유형에 맞는 display로 임시 복원 | |||
*/ | |||
content.style.display = getNaturalDisplay( content ); | |||
content.style.height = 'auto'; | |||
content.style.overflow = 'hidden'; | |||
var targetHeight = content.scrollHeight; | |||
/* | |||
* 0 높이에서 시작 | |||
*/ | |||
content.style.height = '0px'; | |||
/* | |||
* 강제 reflow | |||
*/ | |||
content.offsetHeight; | |||
var animation = content.animate( | |||
[ | |||
{ | { | ||
height: '0px' | |||
}, | |||
{ | |||
height: targetHeight + 'px' | |||
} | } | ||
); | ], | ||
{ | |||
duration: DURATION, | |||
easing: 'ease', | |||
fill: 'forwards' | |||
} | |||
); | |||
animation.onfinish = function () { | |||
content.classList.remove( ANIMATING_CLASS ); | |||
/* | |||
* 원래 CSS가 다시 레이아웃을 결정하도록 | |||
* 인라인 스타일 제거 | |||
*/ | |||
content.style.display = ''; | |||
content.style.height = ''; | |||
content.style.overflow = ''; | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
'true' | |||
); | |||
animation.cancel(); | |||
}; | |||
animation.oncancel = function () { | |||
content.classList.remove( ANIMATING_CLASS ); | |||
} | }; | ||
} | } | ||
/* | /** | ||
* 접기 | * 접기 | ||
*/ | */ | ||
function | function collapse( target, content, toggle ) { | ||
if ( content.classList.contains( ANIMATING_CLASS ) ) { | |||
return; | |||
} | |||
content.classList.add( ANIMATING_CLASS ); | |||
/* | |||
* 현재 높이 측정 | |||
*/ | |||
var startHeight = | |||
content.getBoundingClientRect().height; | |||
/* | |||
* 애니메이션 동안 display 타입 유지 | |||
*/ | |||
content.style.display = getNaturalDisplay( content ); | |||
content.style.height = startHeight + 'px'; | |||
content.style.overflow = 'hidden'; | |||
/* | |||
* 강제 reflow | |||
*/ | |||
content.offsetHeight; | |||
var animation = content.animate( | |||
[ | |||
{ | { | ||
height: startHeight + 'px' | |||
}, | |||
{ | |||
height: '0px' | |||
} | } | ||
], | |||
{ | |||
duration: DURATION, | |||
easing: 'ease', | |||
fill: 'forwards' | |||
} | |||
); | |||
animation.onfinish = function () { | |||
target.classList.add( 'mw-collapsed' ); | |||
content.classList.remove( ANIMATING_CLASS ); | |||
/* | |||
* 접힌 상태 유지 | |||
*/ | |||
content.style.display = 'none'; | |||
/* | |||
* height와 overflow만 제거 | |||
*/ | |||
content.style.height = ''; | |||
content.style.overflow = ''; | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
'false' | |||
); | ); | ||
animation.cancel(); | |||
}; | |||
animation.oncancel = function () { | |||
content.classList.remove( ANIMATING_CLASS ); | |||
}; | |||
} | |||
/** | |||
* 실제 토글 처리 | |||
*/ | |||
function toggleCollapsible( toggle ) { | |||
var name = getToggleName( toggle ); | |||
if ( !name ) { | |||
return false; | |||
} | |||
var target = getTarget( name ); | |||
var target = getTarget( | |||
if ( !target ) { | if ( !target ) { | ||
return; | return false; | ||
} | } | ||
var | var content = getContent( target ); | ||
if ( !content ) { | |||
return false; | |||
if ( ! | |||
return; | |||
} | } | ||
if ( isCollapsed( target, content ) ) { | |||
expand( | |||
target, | |||
content, | |||
toggle | |||
if ( | |||
); | ); | ||
} else { | } else { | ||
collapse( | |||
target, | |||
content, | |||
toggle | |||
); | ); | ||
} | } | ||
return true; | |||
} | } | ||
function | /** | ||
* 마우스 클릭 | |||
* | |||
* MediaWiki 기본 customtoggle보다 먼저 실행하기 위해 | |||
* capture=true 사용 | |||
*/ | |||
document.addEventListener( | |||
'click', | |||
function ( event ) { | |||
var toggle = event.target.closest( | |||
'.mw-customtoggle' | '.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 ( | if ( | ||
! | event.key !== 'Enter' && | ||
event.key !== ' ' | |||
) { | ) { | ||
return; | return; | ||
} | } | ||
toggle. | var toggle = event.target.closest( | ||
' | '.mw-customtoggle' | ||
); | |||
if ( !toggle ) { | |||
return; | |||
} | |||
toggle | if ( !getToggleName( toggle ) ) { | ||
return; | |||
} | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.stopImmediatePropagation(); | |||
toggleCollapsible( toggle ); | |||
}, | |||
true | |||
); | |||
); | |||
}() ); | }() ); | ||
2026년 8월 20일 (목) 13:27 판
/**
* MediaWiki 커스텀 펼치기/접기 애니메이션
*
* 지원 예:
*
* .mw-customtoggle-KSC_LEADER
* ↕
* .mw-customcollapsible-KSC_LEADER
*
* 식별자 이름은 자유롭게 사용 가능.
*/
( function () {
'use strict';
var DURATION = 300;
var ANIMATING_CLASS = 'mw-custom-collapse-animating';
/**
* 토글 요소에서 식별자 추출
*
* 예:
* 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++ ) {
if ( classes[ i ].indexOf( prefix ) === 0 ) {
return classes[ i ].substring( prefix.length );
}
}
return null;
}
/**
* CSS selector escape
*/
function escapeSelector( value ) {
if ( window.CSS && CSS.escape ) {
return CSS.escape( value );
}
return value.replace(
/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g,
'\\\\$1'
);
}
/**
* 식별자에 대응하는 collapsible 대상 찾기
*/
function getTarget( name ) {
if ( !name ) {
return null;
}
var target = document.querySelector(
'.mw-customcollapsible-' +
escapeSelector( name )
);
if ( target ) {
return target;
}
target = document.getElementById(
'mw-customcollapsible-' + name
);
return target;
}
/**
* 실제 애니메이션을 적용할 콘텐츠 찾기
*/
function getContent( target ) {
var content = target.querySelector(
'.mw-collapsible-content'
);
if ( content ) {
return content;
}
return target;
}
/**
* 요소 원래 display 타입 보존
*
* table 요소에 display:block을 강제하면
* 레이아웃이 깨질 수 있으므로 태그별 기본값 사용.
*/
function getNaturalDisplay( element ) {
switch ( element.tagName ) {
case 'TABLE':
return 'table';
case 'TBODY':
return 'table-row-group';
case 'THEAD':
return 'table-header-group';
case 'TFOOT':
return 'table-footer-group';
case 'TR':
return 'table-row';
case 'TD':
case 'TH':
return 'table-cell';
case 'COLGROUP':
return 'table-column-group';
case 'COL':
return 'table-column';
case 'CAPTION':
return 'table-caption';
case 'LI':
return 'list-item';
default:
return 'block';
}
}
/**
* 현재 접혀있는지 확인
*/
function isCollapsed( target, content ) {
if ( target.classList.contains( 'mw-collapsed' ) ) {
return true;
}
return window.getComputedStyle( content ).display === 'none';
}
/**
* 펼치기
*/
function expand( target, content, toggle ) {
if ( content.classList.contains( ANIMATING_CLASS ) ) {
return;
}
content.classList.add( ANIMATING_CLASS );
target.classList.remove( 'mw-collapsed' );
/*
* display:none 상태에서는 높이를 구할 수 없으므로
* 원래 요소 유형에 맞는 display로 임시 복원
*/
content.style.display = getNaturalDisplay( content );
content.style.height = 'auto';
content.style.overflow = 'hidden';
var targetHeight = content.scrollHeight;
/*
* 0 높이에서 시작
*/
content.style.height = '0px';
/*
* 강제 reflow
*/
content.offsetHeight;
var animation = content.animate(
[
{
height: '0px'
},
{
height: targetHeight + 'px'
}
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);
animation.onfinish = function () {
content.classList.remove( ANIMATING_CLASS );
/*
* 원래 CSS가 다시 레이아웃을 결정하도록
* 인라인 스타일 제거
*/
content.style.display = '';
content.style.height = '';
content.style.overflow = '';
toggle.setAttribute(
'aria-expanded',
'true'
);
animation.cancel();
};
animation.oncancel = function () {
content.classList.remove( ANIMATING_CLASS );
};
}
/**
* 접기
*/
function collapse( target, content, toggle ) {
if ( content.classList.contains( ANIMATING_CLASS ) ) {
return;
}
content.classList.add( ANIMATING_CLASS );
/*
* 현재 높이 측정
*/
var startHeight =
content.getBoundingClientRect().height;
/*
* 애니메이션 동안 display 타입 유지
*/
content.style.display = getNaturalDisplay( content );
content.style.height = startHeight + 'px';
content.style.overflow = 'hidden';
/*
* 강제 reflow
*/
content.offsetHeight;
var animation = content.animate(
[
{
height: startHeight + 'px'
},
{
height: '0px'
}
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);
animation.onfinish = function () {
target.classList.add( 'mw-collapsed' );
content.classList.remove( ANIMATING_CLASS );
/*
* 접힌 상태 유지
*/
content.style.display = 'none';
/*
* height와 overflow만 제거
*/
content.style.height = '';
content.style.overflow = '';
toggle.setAttribute(
'aria-expanded',
'false'
);
animation.cancel();
};
animation.oncancel = function () {
content.classList.remove( ANIMATING_CLASS );
};
}
/**
* 실제 토글 처리
*/
function toggleCollapsible( toggle ) {
var name = getToggleName( toggle );
if ( !name ) {
return false;
}
var target = getTarget( name );
if ( !target ) {
return false;
}
var content = getContent( target );
if ( !content ) {
return false;
}
if ( isCollapsed( target, content ) ) {
expand(
target,
content,
toggle
);
} else {
collapse(
target,
content,
toggle
);
}
return true;
}
/**
* 마우스 클릭
*
* MediaWiki 기본 customtoggle보다 먼저 실행하기 위해
* capture=true 사용
*/
document.addEventListener(
'click',
function ( event ) {
var toggle = event.target.closest(
'.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'
);
if ( !toggle ) {
return;
}
if ( !getToggleName( toggle ) ) {
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
toggleCollapsible( toggle );
},
true
);
}() );