편집 요약 없음 태그: 되돌려진 기여 |
편집 요약 없음 태그: 되돌려진 기여 |
||
| 1번째 줄: | 1번째 줄: | ||
/** | /** | ||
* MediaWiki 펼치기/접기 애니메이션 | * MediaWiki 커스텀 펼치기/접기 애니메이션 | ||
* | * | ||
* | * 틀 구조를 수정하지 않고 작동합니다. | ||
* | * | ||
* 구조: | |||
* | |||
* <div id="mw-customcollapsible-XXX" class="mw-collapsible"> | |||
* | |||
* <div class="mw-customtoggle-XXX mw-customtoggle"> | |||
* [ 펼치기 / 접기 ] | |||
* </div> | |||
* | |||
* 실제 내용... | |||
* | |||
* </div> | |||
* | |||
* 토글 버튼은 항상 남겨두고 | |||
* 토글 버튼 뒤의 내용만 애니메이션합니다. | |||
*/ | */ | ||
| 11번째 줄: | 25번째 줄: | ||
var DURATION = 300; | var DURATION = 300; | ||
var BODY_CLASS = 'mw-customcollapse-animation-body'; | |||
var READY_CLASS = 'mw-customcollapse-animation-ready'; | |||
var OPEN_CLASS = 'mw-customcollapse-animation-open'; | |||
var ANIMATING_CLASS = 'mw-customcollapse-animation-running'; | |||
/** | |||
* 토글 이름 추출 | |||
* | |||
* mw-customtoggle-aurann_01 | |||
* → | |||
* aurann_01 | |||
*/ | |||
function getToggleName( toggle ) { | function getToggleName( toggle ) { | ||
var prefix = 'mw-customtoggle-'; | var prefix = 'mw-customtoggle-'; | ||
| 18번째 줄: | 45번째 줄: | ||
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( prefix.length ); | return classes[ i ].substring( | ||
prefix.length | |||
); | |||
} | } | ||
} | } | ||
| 25번째 줄: | 54번째 줄: | ||
} | } | ||
function getTarget( name ) | |||
/** | |||
* 토글과 연결된 부모 찾기 | |||
*/ | |||
function getTarget( toggle ) { | |||
var name = getToggleName( toggle ); | |||
if ( !name ) { | if ( !name ) { | ||
return null; | return null; | ||
} | } | ||
/* | |||
* 현재 구조에서는 toggle 자체가 | |||
* collapsible 안에 있으므로 우선 closest 사용 | |||
*/ | |||
var target = toggle.closest( | |||
'.mw-collapsible' | |||
); | |||
if ( | |||
target && | |||
target.id === | |||
'mw-customcollapsible-' + name | |||
) { | |||
return target; | |||
} | |||
/* | |||
* 혹시 구조가 다를 경우 ID로 검색 | |||
*/ | |||
return document.getElementById( | return document.getElementById( | ||
'mw-customcollapsible-' + name | 'mw-customcollapsible-' + name | ||
| 35번째 줄: | 89번째 줄: | ||
} | } | ||
/* | |||
* | /** | ||
* | * 토글 버튼 이후의 모든 내용을 | ||
* 애니메이션용 DIV로 감싼다. | |||
* | * | ||
* | * 틀 원본 HTML은 수정할 필요 없음. | ||
*/ | */ | ||
function | function createAnimationBody( target, toggle ) { | ||
var | var existing = target.querySelector( | ||
'. | ':scope > .' + BODY_CLASS | ||
); | ); | ||
if ( | if ( existing ) { | ||
return | return existing; | ||
} | } | ||
var body = document.createElement( 'div' ); | |||
body.className = BODY_CLASS; | |||
/* | /* | ||
* | * toggle 다음의 모든 노드를 body 안으로 이동 | ||
* | |||
* table뿐만 아니라 div, 텍스트 등도 지원 | |||
*/ | */ | ||
var | var node = toggle.nextSibling; | ||
var nodes = []; | |||
while ( node ) { | |||
nodes.push( node ); | |||
node = node.nextSibling; | |||
} | } | ||
nodes.forEach( function ( child ) { | |||
body.appendChild( child ); | |||
} ); | |||
target.appendChild( body ); | |||
return body; | |||
} | |||
/** | |||
* 초기 상태 확인 | |||
*/ | |||
function getInitialOpenState( target, toggle, body ) { | |||
/* | /* | ||
* | * MediaWiki가 mw-collapsed를 붙였다면 접힌 상태 | ||
*/ | */ | ||
if ( target. | if ( target.classList.contains( 'mw-collapsed' ) ) { | ||
return false; | |||
} | |||
/* | |||
* body가 display:none이면 접힌 상태 | |||
*/ | |||
if ( | |||
window.getComputedStyle( body ).display === 'none' | |||
) { | |||
return false; | |||
} | |||
/* | |||
* aria-expanded가 명시적으로 false | |||
*/ | |||
if ( | |||
toggle.getAttribute( 'aria-expanded' ) === | |||
'false' | |||
) { | |||
return false; | |||
} | } | ||
return | return true; | ||
} | } | ||
function | |||
if ( | /** | ||
* 펼치기 | |||
*/ | |||
function openTarget( target, toggle, body ) { | |||
if ( | |||
target.classList.contains( | |||
ANIMATING_CLASS | |||
) | |||
) { | |||
return; | return; | ||
} | } | ||
target.classList.add( | |||
ANIMATING_CLASS | |||
); | |||
target.classList.remove( | |||
'mw-collapsed' | |||
); | |||
body.style.display = 'block'; | |||
body.style.overflow = 'hidden'; | |||
/* | |||
* 실제 최종 높이 계산 | |||
*/ | |||
body.style.height = 'auto'; | |||
var endHeight = body.scrollHeight; | |||
/* | |||
* 0부터 시작 | |||
*/ | |||
body.style.height = '0px'; | |||
/* | |||
* 강제 reflow | |||
*/ | |||
body.offsetHeight; | |||
var animation = | var animation = body.animate( | ||
[ | [ | ||
{ height: '0px' }, | { | ||
{ height: | height: '0px' | ||
}, | |||
{ | |||
height: endHeight + 'px' | |||
} | |||
], | ], | ||
{ | { | ||
duration: DURATION, | duration: DURATION, | ||
easing: 'ease', | easing: 'ease-out', | ||
fill: 'forwards' | fill: 'forwards' | ||
} | } | ||
| 109번째 줄: | 223번째 줄: | ||
animation.onfinish = function () { | animation.onfinish = function () { | ||
target.classList.remove( | |||
ANIMATING_CLASS | |||
); | |||
target.classList.add( | |||
OPEN_CLASS | |||
); | |||
body.style.height = ''; | |||
body.style.overflow = ''; | |||
body.style.display = ''; | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
'true' | |||
); | |||
animation.cancel(); | animation.cancel(); | ||
| 116번째 줄: | 244번째 줄: | ||
} | } | ||
function | |||
if ( | /** | ||
* 접기 | |||
*/ | |||
function closeTarget( target, toggle, body ) { | |||
if ( | |||
target.classList.contains( | |||
ANIMATING_CLASS | |||
) | |||
) { | |||
return; | return; | ||
} | } | ||
var | target.classList.add( | ||
ANIMATING_CLASS | |||
); | |||
var startHeight = | |||
body.getBoundingClientRect().height; | |||
/* | |||
* 이미 접혀있는 경우 | |||
*/ | |||
if ( startHeight <= 0 ) { | |||
target.classList.remove( | |||
ANIMATING_CLASS | |||
); | |||
return; | return; | ||
} | } | ||
body.style.height = | |||
startHeight + 'px'; | |||
body.style.overflow = | |||
'hidden'; | |||
/* | |||
* 강제 reflow | |||
*/ | |||
body.offsetHeight; | |||
var animation = | var animation = body.animate( | ||
[ | [ | ||
{ height: | { | ||
{ height: '0px' } | height: | ||
startHeight + 'px' | |||
}, | |||
{ | |||
height: '0px' | |||
} | |||
], | ], | ||
{ | { | ||
duration: DURATION, | duration: DURATION, | ||
easing: 'ease', | easing: 'ease-in', | ||
fill: 'forwards' | fill: 'forwards' | ||
} | } | ||
| 144번째 줄: | 304번째 줄: | ||
animation.onfinish = function () { | animation.onfinish = function () { | ||
target.classList.remove( | |||
ANIMATING_CLASS | |||
); | |||
target.classList.remove( | |||
OPEN_CLASS | |||
); | |||
target.classList.add( | |||
'mw-collapsed' | |||
); | |||
body.style.display = 'none'; | |||
body.style.height = ''; | |||
body.style.overflow = ''; | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
'false' | |||
); | |||
animation.cancel(); | animation.cancel(); | ||
| 151번째 줄: | 329번째 줄: | ||
} | } | ||
/** | |||
* 실제 클릭 처리 | |||
*/ | |||
function toggleTarget( toggle ) { | |||
var target = getTarget( toggle ); | |||
if ( !target ) { | |||
return false; | |||
} | |||
var body = target.querySelector( | |||
':scope > .' + BODY_CLASS | |||
); | |||
if ( !body ) { | |||
body = createAnimationBody( | |||
target, | |||
toggle | |||
); | |||
} | |||
if ( !body ) { | |||
return false; | |||
} | |||
var isOpen = | |||
toggle.getAttribute( | |||
'aria-expanded' | |||
) === 'true' && | |||
!target.classList.contains( | |||
'mw-collapsed' | |||
); | |||
if ( isOpen ) { | |||
closeTarget( | |||
target, | |||
toggle, | |||
body | |||
); | |||
} else { | |||
openTarget( | |||
target, | |||
toggle, | |||
body | |||
); | |||
} | |||
return true; | |||
} | |||
/** | |||
* 각 custom collapsible 초기화 | |||
*/ | |||
function initializeTarget( toggle ) { | |||
var target = getTarget( toggle ); | |||
if ( !target ) { | |||
return; | |||
} | |||
if ( | |||
target.classList.contains( | |||
READY_CLASS | |||
) | |||
) { | |||
return; | |||
} | |||
var body = createAnimationBody( | |||
target, | |||
toggle | |||
); | |||
if ( !body ) { | |||
return; | |||
} | |||
target.classList.add( | |||
READY_CLASS | |||
); | |||
var isOpen = getInitialOpenState( | |||
target, | |||
toggle, | |||
body | |||
); | |||
if ( isOpen ) { | |||
target.classList.add( | |||
OPEN_CLASS | |||
); | |||
target.classList.remove( | |||
'mw-collapsed' | |||
); | |||
body.style.display = ''; | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
'true' | |||
); | |||
} else { | |||
target.classList.remove( | |||
OPEN_CLASS | |||
); | |||
target.classList.add( | |||
'mw-collapsed' | |||
); | |||
body.style.display = 'none'; | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
'false' | |||
); | |||
} | |||
} | |||
/** | |||
* 페이지 전체 초기화 | |||
*/ | |||
function initializeAll() { | |||
var toggles = document.querySelectorAll( | |||
'.mw-customtoggle' | |||
); | |||
toggles.forEach( | |||
function ( toggle ) { | |||
if ( getToggleName( toggle ) ) { | |||
initializeTarget( | |||
toggle | |||
); | |||
} | |||
} | |||
); | |||
} | |||
/** | |||
* 클릭 | |||
* | |||
* MediaWiki 기본 토글보다 먼저 실행해서 | |||
* 기본 display:none 처리 방지. | |||
*/ | |||
document.addEventListener( | document.addEventListener( | ||
'click', | 'click', | ||
| 163번째 줄: | 490번째 줄: | ||
} | } | ||
if ( | |||
!getToggleName( toggle ) | |||
) { | |||
return; | return; | ||
} | } | ||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.stopImmediatePropagation(); | |||
toggleTarget( | |||
toggle | |||
); | |||
}, | |||
true | |||
); | |||
if ( ! | |||
/** | |||
* Enter / Space | |||
*/ | |||
document.addEventListener( | |||
'keydown', | |||
function ( event ) { | |||
if ( | |||
event.key !== 'Enter' && | |||
event.key !== ' ' | |||
) { | |||
return; | return; | ||
} | } | ||
var | var toggle = | ||
event.target.closest( | |||
'.mw-customtoggle' | |||
); | ); | ||
if ( ! | if ( !toggle ) { | ||
return; | return; | ||
} | } | ||
if ( | |||
!getToggleName( toggle ) | |||
) { | |||
return; | |||
} | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.stopImmediatePropagation(); | |||
toggleTarget( | |||
toggle | |||
); | ); | ||
}, | }, | ||
true | |||
); | ); | ||
/** | |||
* 초기 실행 | |||
*/ | |||
if ( | |||
document.readyState === | |||
'loading' | |||
) { | |||
document.addEventListener( | |||
'DOMContentLoaded', | |||
initializeAll | |||
); | |||
} else { | |||
initializeAll(); | |||
} | |||
/** | |||
* MediaWiki AJAX/동적 콘텐츠 대응 | |||
*/ | |||
if ( | |||
window.mw && | |||
mw.hook | |||
) { | |||
mw.hook( | |||
'wikipage.content' | |||
).add( | |||
function () { | |||
initializeAll(); | |||
} | |||
); | |||
} | |||
}() ); | }() ); | ||
2026년 8월 20일 (목) 13:23 판
/**
* MediaWiki 커스텀 펼치기/접기 애니메이션
*
* 틀 구조를 수정하지 않고 작동합니다.
*
* 구조:
*
* <div id="mw-customcollapsible-XXX" class="mw-collapsible">
*
* <div class="mw-customtoggle-XXX mw-customtoggle">
* [ 펼치기 / 접기 ]
* </div>
*
* 실제 내용...
*
* </div>
*
* 토글 버튼은 항상 남겨두고
* 토글 버튼 뒤의 내용만 애니메이션합니다.
*/
( function () {
'use strict';
var DURATION = 300;
var BODY_CLASS = 'mw-customcollapse-animation-body';
var READY_CLASS = 'mw-customcollapse-animation-ready';
var OPEN_CLASS = 'mw-customcollapse-animation-open';
var ANIMATING_CLASS = 'mw-customcollapse-animation-running';
/**
* 토글 이름 추출
*
* mw-customtoggle-aurann_01
* →
* aurann_01
*/
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;
}
/*
* 현재 구조에서는 toggle 자체가
* collapsible 안에 있으므로 우선 closest 사용
*/
var target = toggle.closest(
'.mw-collapsible'
);
if (
target &&
target.id ===
'mw-customcollapsible-' + name
) {
return target;
}
/*
* 혹시 구조가 다를 경우 ID로 검색
*/
return document.getElementById(
'mw-customcollapsible-' + name
);
}
/**
* 토글 버튼 이후의 모든 내용을
* 애니메이션용 DIV로 감싼다.
*
* 틀 원본 HTML은 수정할 필요 없음.
*/
function createAnimationBody( target, toggle ) {
var existing = target.querySelector(
':scope > .' + BODY_CLASS
);
if ( existing ) {
return existing;
}
var body = document.createElement( 'div' );
body.className = BODY_CLASS;
/*
* toggle 다음의 모든 노드를 body 안으로 이동
*
* table뿐만 아니라 div, 텍스트 등도 지원
*/
var node = toggle.nextSibling;
var nodes = [];
while ( node ) {
nodes.push( node );
node = node.nextSibling;
}
nodes.forEach( function ( child ) {
body.appendChild( child );
} );
target.appendChild( body );
return body;
}
/**
* 초기 상태 확인
*/
function getInitialOpenState( target, toggle, body ) {
/*
* MediaWiki가 mw-collapsed를 붙였다면 접힌 상태
*/
if ( target.classList.contains( 'mw-collapsed' ) ) {
return false;
}
/*
* body가 display:none이면 접힌 상태
*/
if (
window.getComputedStyle( body ).display === 'none'
) {
return false;
}
/*
* aria-expanded가 명시적으로 false
*/
if (
toggle.getAttribute( 'aria-expanded' ) ===
'false'
) {
return false;
}
return true;
}
/**
* 펼치기
*/
function openTarget( target, toggle, body ) {
if (
target.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
target.classList.add(
ANIMATING_CLASS
);
target.classList.remove(
'mw-collapsed'
);
body.style.display = 'block';
body.style.overflow = 'hidden';
/*
* 실제 최종 높이 계산
*/
body.style.height = 'auto';
var endHeight = body.scrollHeight;
/*
* 0부터 시작
*/
body.style.height = '0px';
/*
* 강제 reflow
*/
body.offsetHeight;
var animation = body.animate(
[
{
height: '0px'
},
{
height: endHeight + 'px'
}
],
{
duration: DURATION,
easing: 'ease-out',
fill: 'forwards'
}
);
animation.onfinish = function () {
target.classList.remove(
ANIMATING_CLASS
);
target.classList.add(
OPEN_CLASS
);
body.style.height = '';
body.style.overflow = '';
body.style.display = '';
toggle.setAttribute(
'aria-expanded',
'true'
);
animation.cancel();
};
}
/**
* 접기
*/
function closeTarget( target, toggle, body ) {
if (
target.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
target.classList.add(
ANIMATING_CLASS
);
var startHeight =
body.getBoundingClientRect().height;
/*
* 이미 접혀있는 경우
*/
if ( startHeight <= 0 ) {
target.classList.remove(
ANIMATING_CLASS
);
return;
}
body.style.height =
startHeight + 'px';
body.style.overflow =
'hidden';
/*
* 강제 reflow
*/
body.offsetHeight;
var animation = body.animate(
[
{
height:
startHeight + 'px'
},
{
height: '0px'
}
],
{
duration: DURATION,
easing: 'ease-in',
fill: 'forwards'
}
);
animation.onfinish = function () {
target.classList.remove(
ANIMATING_CLASS
);
target.classList.remove(
OPEN_CLASS
);
target.classList.add(
'mw-collapsed'
);
body.style.display = 'none';
body.style.height = '';
body.style.overflow = '';
toggle.setAttribute(
'aria-expanded',
'false'
);
animation.cancel();
};
}
/**
* 실제 클릭 처리
*/
function toggleTarget( toggle ) {
var target = getTarget( toggle );
if ( !target ) {
return false;
}
var body = target.querySelector(
':scope > .' + BODY_CLASS
);
if ( !body ) {
body = createAnimationBody(
target,
toggle
);
}
if ( !body ) {
return false;
}
var isOpen =
toggle.getAttribute(
'aria-expanded'
) === 'true' &&
!target.classList.contains(
'mw-collapsed'
);
if ( isOpen ) {
closeTarget(
target,
toggle,
body
);
} else {
openTarget(
target,
toggle,
body
);
}
return true;
}
/**
* 각 custom collapsible 초기화
*/
function initializeTarget( toggle ) {
var target = getTarget( toggle );
if ( !target ) {
return;
}
if (
target.classList.contains(
READY_CLASS
)
) {
return;
}
var body = createAnimationBody(
target,
toggle
);
if ( !body ) {
return;
}
target.classList.add(
READY_CLASS
);
var isOpen = getInitialOpenState(
target,
toggle,
body
);
if ( isOpen ) {
target.classList.add(
OPEN_CLASS
);
target.classList.remove(
'mw-collapsed'
);
body.style.display = '';
toggle.setAttribute(
'aria-expanded',
'true'
);
} else {
target.classList.remove(
OPEN_CLASS
);
target.classList.add(
'mw-collapsed'
);
body.style.display = 'none';
toggle.setAttribute(
'aria-expanded',
'false'
);
}
}
/**
* 페이지 전체 초기화
*/
function initializeAll() {
var toggles = document.querySelectorAll(
'.mw-customtoggle'
);
toggles.forEach(
function ( toggle ) {
if ( getToggleName( toggle ) ) {
initializeTarget(
toggle
);
}
}
);
}
/**
* 클릭
*
* MediaWiki 기본 토글보다 먼저 실행해서
* 기본 display:none 처리 방지.
*/
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();
toggleTarget(
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();
toggleTarget(
toggle
);
},
true
);
/**
* 초기 실행
*/
if (
document.readyState ===
'loading'
) {
document.addEventListener(
'DOMContentLoaded',
initializeAll
);
} else {
initializeAll();
}
/**
* MediaWiki AJAX/동적 콘텐츠 대응
*/
if (
window.mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(
function () {
initializeAll();
}
);
}
}() );